@@ -31,6 +31,17 @@ DATABASE_PASSWORD="" |
||
31 | 31 |
# Configure Rails environment. This should only be needed in production and may cause errors in development. |
32 | 32 |
# RAILS_ENV=production |
33 | 33 |
|
34 |
+# Should Rails force all requests to use SSL? |
|
35 |
+FORCE_SSL=false |
|
36 |
+ |
|
37 |
+############################ |
|
38 |
+# Allowing Signups # |
|
39 |
+############################ |
|
40 |
+ |
|
41 |
+# This invitation code will be required for users to signup with your Huginn installation. |
|
42 |
+# You can see its use in user.rb. PLEASE CHANGE THIS! |
|
43 |
+INVITATION_CODE=try-huginn |
|
44 |
+ |
|
34 | 45 |
############################# |
35 | 46 |
# Email Configuration # |
36 | 47 |
############################# |
@@ -52,13 +63,6 @@ SMTP_ENABLE_STARTTLS_AUTO=true |
||
52 | 63 |
# The address from which system emails will appear to be sent. |
53 | 64 |
EMAIL_FROM_ADDRESS=from_address@gmail.com |
54 | 65 |
|
55 |
-############################ |
|
56 |
-# Allowing Signups # |
|
57 |
-############################ |
|
58 |
- |
|
59 |
-# This invitation code will be required for users to signup with your Huginn installation. |
|
60 |
-# You can see its use in user.rb. |
|
61 |
-INVITATION_CODE=try-huginn |
|
62 | 66 |
|
63 | 67 |
########################### |
64 | 68 |
# Agent Logging # |
@@ -77,3 +81,27 @@ AWS_ACCESS_KEY="your aws access key" |
||
77 | 81 |
|
78 | 82 |
# Set AWS_SANDBOX to true if you're developing Huginn code. |
79 | 83 |
AWS_SANDBOX=false |
84 |
+ |
|
85 |
+######################## |
|
86 |
+# Various Settings # |
|
87 |
+######################## |
|
88 |
+ |
|
89 |
+# Specify the HTTP backend library for Faraday, used in WebsiteAgent. |
|
90 |
+# You can change this depending on the performance and stability you |
|
91 |
+# need for your service. Any choice other than "typhoeus", |
|
92 |
+# "net_http", or "em_http" should require you to bundle a corresponding |
|
93 |
+# gem via Gemfile. |
|
94 |
+FARADAY_HTTP_BACKEND=typhoeus |
|
95 |
+ |
|
96 |
+# Allow JSONPath eval expresions. i.e., $..price[?(@ < 20)] |
|
97 |
+# You should not allow this on a shared Huginn box because it is not secure. |
|
98 |
+ALLOW_JSONPATH_EVAL=false |
|
99 |
+ |
|
100 |
+# Enable this setting to allow insecure Agents like the ShellCommandAgent. Only do this |
|
101 |
+# when you trust everyone using your Huginn installation. |
|
102 |
+ENABLE_INSECURE_AGENTS=false |
|
103 |
+ |
|
104 |
+# Use Graphviz for generating diagrams instead of using Google Chart |
|
105 |
+# Tools. Specify a dot(1) command path built with SVG support |
|
106 |
+# enabled. |
|
107 |
+#USE_GRAPHVIZ_DOT=dot |
@@ -7,6 +7,7 @@ capybara-*.html |
||
7 | 7 |
/vendor/bundle |
8 | 8 |
/log/* |
9 | 9 |
/tmp/* |
10 |
+!/tmp/.gitkeep |
|
10 | 11 |
/db/*.sqlite3 |
11 | 12 |
/public/system/* |
12 | 13 |
/coverage/ |
@@ -1,9 +1,13 @@ |
||
1 | 1 |
language: ruby |
2 | 2 |
bundler_args: --without development production |
3 |
+env: |
|
4 |
+ - APP_SECRET_TOKEN=b2724973fd81c2f4ac0f92ac48eb3f0152c4a11824c122bcf783419a4c51d8b9bba81c8ba6a66c7de599677c7f486242cf819775c433908e77c739c5c8ae118d |
|
3 | 5 |
rvm: |
4 | 6 |
- 2.0.0 |
5 | 7 |
- 2.1.1 |
6 | 8 |
- 1.9.3 |
9 |
+before_install: |
|
10 |
+ - travis_retry gem install bundler |
|
7 | 11 |
before_script: |
8 | 12 |
- mysql -e 'create database huginn_test;' |
9 | 13 |
- bundle exec rake db:migrate db:test:prepare |
@@ -13,6 +17,6 @@ notifications: |
||
13 | 17 |
channels: |
14 | 18 |
- "chat.freenode.net#huginn" |
15 | 19 |
template: |
16 |
- - "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}" |
|
20 |
+ - "<%{author}> %{branch} - %{commit} (%{commit_message}): %{message}" |
|
17 | 21 |
- "Change view : %{compare_url}" |
18 | 22 |
- "Build details : %{build_url}" |
@@ -1,6 +1,7 @@ |
||
1 | 1 |
# Changes |
2 | 2 |
|
3 |
-* 0.4 (April 10, 2014) - WebHooksController has been renamed to WebRequestsController and all HTTP verbs are now accepted and passed through to Agents' #receive_web_request method. The new DataOutputAgent returns JSON or RSS feeds of incoming Events via external web request. |
|
3 |
+* 0.5 (April 20, 2014) - Tons of new additions! FtpsiteAgent; WebsiteAgent has xpath, multiple URL, and encoding support; regexp extractions in EventFormattingAgent; PostAgent takes default params and headers, and can make GET requests; local Graphviz support; ShellCommandAgent; BasecampAgent; HipchatAgent; and lots of bug fixes! |
|
4 |
+* 0.4 (April 10, 2014) - WebHooksController has been renamed to WebRequestsController and all HTTP verbs are now accepted and passed through to Agents' #receive\_web\_request method. The new DataOutputAgent returns JSON or RSS feeds of incoming Events via external web request. [Documentation is on the wiki.](https://github.com/cantino/huginn/wiki/Creating-a-new-agent#receiving-web-requests). |
|
4 | 5 |
* 0.31 (Jan 2, 2014) - Agents now have an optional keep\_events\_for option that is propagated to created events' expires\_at field, and they update their events' expires\_at fields on change. |
5 | 6 |
* 0.3 (Jan 1, 2014) - Remove symbolization of memory, options, and payloads; convert memory, options, and payloads to JSON from YAML. Migration will perform conversion and adjust tables to be UTF-8. Recommend making a DB backup before migrating. |
6 | 7 |
* 0.2 (Nov 6, 2013) - PeakDetectorAgent now uses `window_duration_in_days` and `min_peak_spacing_in_days`. Additionally, peaks trigger when the time series rises over the standard deviation multiple, not after it starts to fall. |
@@ -1,67 +1,86 @@ |
||
1 | 1 |
source 'https://rubygems.org' |
2 | 2 |
|
3 |
-gem 'rails' |
|
4 |
-gem 'rake' |
|
5 |
-gem 'mysql2' |
|
6 |
-gem 'devise' |
|
7 |
-gem 'kaminari' |
|
8 |
-gem 'bootstrap-kaminari-views' |
|
9 |
-gem "rufus-scheduler", :require => false |
|
10 |
-gem 'json', '>= 1.7.7' |
|
11 |
-gem 'jsonpath' |
|
12 |
-gem 'twilio-ruby' |
|
13 |
-gem 'ruby-growl' |
|
14 |
- |
|
15 |
-gem 'delayed_job' |
|
16 |
-gem 'delayed_job_active_record'#, "~> 0.3.3" # newer was giving a strange MySQL error |
|
17 |
-gem "daemons" |
|
3 |
+gem 'protected_attributes', '~>1.0.7' |
|
18 | 4 |
|
19 |
-# To enable DelayedJobWeb, see the 'Enable DelayedJobWeb' section of the README. |
|
20 |
-# gem "delayed_job_web" |
|
21 |
- |
|
22 |
-gem 'foreman' |
|
23 |
-gem 'dotenv-rails', :groups => [:development, :test] |
|
24 |
- |
|
25 |
-gem 'sass-rails', '~> 3.2.3' |
|
26 |
-gem 'coffee-rails', '~> 3.2.1' |
|
27 |
-gem 'uglifier', '>= 1.0.3' |
|
28 |
-gem 'select2-rails' |
|
29 |
-gem 'jquery-rails' |
|
30 |
-gem 'ace-rails-ap' |
|
31 |
- |
|
32 |
-gem 'geokit-rails3' |
|
33 |
-gem 'kramdown' |
|
34 |
-gem "typhoeus" |
|
35 |
-gem 'nokogiri' |
|
36 |
-gem 'wunderground' |
|
37 |
-gem 'forecast_io' |
|
38 |
-gem 'rturk' |
|
39 |
- |
|
40 |
-gem "twitter", '~> 5.7.1' |
|
41 |
-gem 'twitter-stream', :git => 'https://github.com/cantino/twitter-stream', :branch => 'master' |
|
42 |
-gem 'em-http-request' |
|
43 |
-gem 'weibo_2' |
|
44 |
- |
|
45 |
-gem 'xmpp4r', '~> 0.5.6' |
|
46 |
- |
|
47 |
-gem 'therubyracer' |
|
48 |
- |
|
49 |
-platforms :ruby_18 do |
|
50 |
- gem 'system_timer' |
|
51 |
- gem 'fastercsv' |
|
5 |
+gem 'rails', '4.1.0' |
|
6 |
+ |
|
7 |
+case RUBY_PLATFORM |
|
8 |
+when /freebsd/i |
|
9 |
+ # Seems FreeBSD's zoneinfo is not exactly what tzinfo expects |
|
10 |
+ gem 'tzinfo-data' |
|
11 |
+else |
|
12 |
+ # Windows does not include zoneinfo files, so bundle the tzinfo-data gem |
|
13 |
+ gem 'tzinfo-data', platforms: [:mswin] |
|
52 | 14 |
end |
53 | 15 |
|
16 |
+gem 'mysql2', '~> 0.3.15' |
|
17 |
+gem 'devise', '~> 3.2.4' |
|
18 |
+gem 'kaminari', '~> 0.15.1' |
|
19 |
+gem 'bootstrap-kaminari-views', '~> 0.0.2' |
|
20 |
+gem 'rufus-scheduler', '~> 3.0.7', require: false |
|
21 |
+gem 'json', '~> 1.8.1' |
|
22 |
+gem 'jsonpath', '~> 0.5.3' |
|
23 |
+gem 'twilio-ruby', '~> 3.11.5' |
|
24 |
+gem 'ruby-growl', '~> 4.1.0' |
|
25 |
+ |
|
26 |
+gem 'delayed_job', '~> 4.0.0' |
|
27 |
+gem 'delayed_job_active_record', '~> 4.0.0' |
|
28 |
+gem 'daemons', '~> 1.1.9' |
|
29 |
+ |
|
30 |
+# To enable DelayedJobWeb, see the 'Enable DelayedJobWeb' section of the README. |
|
31 |
+# gem 'delayed_job_web' |
|
32 |
+ |
|
33 |
+gem 'foreman', '~> 0.63.0' |
|
34 |
+ |
|
35 |
+gem 'sass-rails', '~> 4.0.0' |
|
36 |
+gem 'coffee-rails', '~> 4.0.0' |
|
37 |
+gem 'uglifier', '>= 1.3.0' |
|
38 |
+gem 'select2-rails', '~> 3.5.4' |
|
39 |
+gem 'jquery-rails', '~> 3.1.0' |
|
40 |
+gem 'ace-rails-ap', '~> 2.0.1' |
|
41 |
+ |
|
42 |
+# geokit-rails doesn't work with geokit 1.8.X but it specifies ~> 1.5 |
|
43 |
+# in its own Gemfile. |
|
44 |
+gem 'geokit', '~> 1.8.4' |
|
45 |
+gem 'geokit-rails', '~> 2.0.1' |
|
46 |
+ |
|
47 |
+gem 'kramdown', '~> 1.3.3' |
|
48 |
+gem 'faraday', '~> 0.9.0' |
|
49 |
+gem 'faraday_middleware' |
|
50 |
+gem 'typhoeus', '~> 0.6.3' |
|
51 |
+gem 'nokogiri', '~> 1.6.1' |
|
52 |
+ |
|
53 |
+gem 'wunderground', '~> 1.2.0' |
|
54 |
+gem 'forecast_io', '~> 2.0.0' |
|
55 |
+gem 'rturk', '~> 2.12.1' |
|
56 |
+ |
|
57 |
+gem 'twitter', '~> 5.8.0' |
|
58 |
+gem 'twitter-stream', github: 'cantino/twitter-stream', branch: 'master' |
|
59 |
+gem 'em-http-request', '~> 1.1.2' |
|
60 |
+gem 'weibo_2', '~> 0.1.4' |
|
61 |
+gem 'hipchat', '~> 1.1.0' |
|
62 |
+gem 'xmpp4r', '~> 0.5.6' |
|
63 |
+ |
|
64 |
+gem 'therubyracer', '~> 0.12.1' |
|
65 |
+ |
|
54 | 66 |
group :development do |
55 | 67 |
gem 'binding_of_caller' |
56 | 68 |
gem 'better_errors' |
57 | 69 |
end |
58 | 70 |
|
59 | 71 |
group :development, :test do |
72 |
+ gem 'dotenv-rails' |
|
60 | 73 |
gem 'pry' |
61 | 74 |
gem 'rspec-rails' |
62 | 75 |
gem 'rspec' |
63 | 76 |
gem 'shoulda-matchers' |
64 | 77 |
gem 'rr' |
65 |
- gem 'webmock', :require => false |
|
66 |
- gem 'coveralls', :require => false |
|
78 |
+ gem 'delorean' |
|
79 |
+ gem 'webmock', require: false |
|
80 |
+ gem 'coveralls', require: false |
|
81 |
+end |
|
82 |
+ |
|
83 |
+group :production do |
|
84 |
+ gem 'dotenv-deployment' |
|
85 |
+ gem 'rack' |
|
67 | 86 |
end |
@@ -1,5 +1,5 @@ |
||
1 | 1 |
GIT |
2 |
- remote: https://github.com/cantino/twitter-stream |
|
2 |
+ remote: git://github.com/cantino/twitter-stream.git |
|
3 | 3 |
revision: fde6bed2b62ca487d49e4a57381bbfca6e33361b |
4 | 4 |
branch: master |
5 | 5 |
specs: |
@@ -12,81 +12,85 @@ GEM |
||
12 | 12 |
remote: https://rubygems.org/ |
13 | 13 |
specs: |
14 | 14 |
ace-rails-ap (2.0.1) |
15 |
- actionmailer (3.2.13) |
|
16 |
- actionpack (= 3.2.13) |
|
17 |
- mail (~> 2.5.3) |
|
18 |
- actionpack (3.2.13) |
|
19 |
- activemodel (= 3.2.13) |
|
20 |
- activesupport (= 3.2.13) |
|
21 |
- builder (~> 3.0.0) |
|
15 |
+ actionmailer (4.1.0) |
|
16 |
+ actionpack (= 4.1.0) |
|
17 |
+ actionview (= 4.1.0) |
|
18 |
+ mail (~> 2.5.4) |
|
19 |
+ actionpack (4.1.0) |
|
20 |
+ actionview (= 4.1.0) |
|
21 |
+ activesupport (= 4.1.0) |
|
22 |
+ rack (~> 1.5.2) |
|
23 |
+ rack-test (~> 0.6.2) |
|
24 |
+ actionview (4.1.0) |
|
25 |
+ activesupport (= 4.1.0) |
|
26 |
+ builder (~> 3.1) |
|
22 | 27 |
erubis (~> 2.7.0) |
23 |
- journey (~> 1.0.4) |
|
24 |
- rack (~> 1.4.5) |
|
25 |
- rack-cache (~> 1.2) |
|
26 |
- rack-test (~> 0.6.1) |
|
27 |
- sprockets (~> 2.2.1) |
|
28 |
- activemodel (3.2.13) |
|
29 |
- activesupport (= 3.2.13) |
|
30 |
- builder (~> 3.0.0) |
|
31 |
- activerecord (3.2.13) |
|
32 |
- activemodel (= 3.2.13) |
|
33 |
- activesupport (= 3.2.13) |
|
34 |
- arel (~> 3.0.2) |
|
35 |
- tzinfo (~> 0.3.29) |
|
36 |
- activeresource (3.2.13) |
|
37 |
- activemodel (= 3.2.13) |
|
38 |
- activesupport (= 3.2.13) |
|
39 |
- activesupport (3.2.13) |
|
40 |
- i18n (= 0.6.1) |
|
41 |
- multi_json (~> 1.0) |
|
42 |
- addressable (2.3.5) |
|
43 |
- arel (3.0.3) |
|
44 |
- atomic (1.1.14) |
|
45 |
- bcrypt-ruby (3.1.1) |
|
28 |
+ activemodel (4.1.0) |
|
29 |
+ activesupport (= 4.1.0) |
|
30 |
+ builder (~> 3.1) |
|
31 |
+ activerecord (4.1.0) |
|
32 |
+ activemodel (= 4.1.0) |
|
33 |
+ activesupport (= 4.1.0) |
|
34 |
+ arel (~> 5.0.0) |
|
35 |
+ activesupport (4.1.0) |
|
36 |
+ i18n (~> 0.6, >= 0.6.9) |
|
37 |
+ json (~> 1.7, >= 1.7.7) |
|
38 |
+ minitest (~> 5.1) |
|
39 |
+ thread_safe (~> 0.1) |
|
40 |
+ tzinfo (~> 1.1) |
|
41 |
+ addressable (2.3.6) |
|
42 |
+ arel (5.0.1.20140414130214) |
|
43 |
+ bcrypt (3.1.7) |
|
46 | 44 |
better_errors (1.1.0) |
47 | 45 |
coderay (>= 1.0.0) |
48 | 46 |
erubis (>= 2.6.6) |
49 | 47 |
binding_of_caller (0.7.2) |
50 | 48 |
debug_inspector (>= 0.0.1) |
51 |
- bootstrap-kaminari-views (0.0.2) |
|
49 |
+ bootstrap-kaminari-views (0.0.3) |
|
52 | 50 |
kaminari (>= 0.13) |
53 | 51 |
rails (>= 3.1) |
54 | 52 |
buftok (0.2.0) |
55 |
- builder (3.0.4) |
|
56 |
- coderay (1.0.9) |
|
57 |
- coffee-rails (3.2.2) |
|
53 |
+ builder (3.2.2) |
|
54 |
+ chronic (0.10.2) |
|
55 |
+ coderay (1.1.0) |
|
56 |
+ coffee-rails (4.0.1) |
|
58 | 57 |
coffee-script (>= 2.2.0) |
59 |
- railties (~> 3.2.0) |
|
58 |
+ railties (>= 4.0.0, < 5.0) |
|
60 | 59 |
coffee-script (2.2.0) |
61 | 60 |
coffee-script-source |
62 | 61 |
execjs |
63 |
- coffee-script-source (1.6.3) |
|
64 |
- cookiejar (0.3.1) |
|
62 |
+ coffee-script-source (1.7.0) |
|
63 |
+ cookiejar (0.3.2) |
|
65 | 64 |
coveralls (0.7.0) |
66 | 65 |
multi_json (~> 1.3) |
67 | 66 |
rest-client |
68 | 67 |
simplecov (>= 0.7) |
69 | 68 |
term-ansicolor |
70 | 69 |
thor |
71 |
- crack (0.4.1) |
|
72 |
- safe_yaml (~> 0.9.0) |
|
70 |
+ crack (0.4.2) |
|
71 |
+ safe_yaml (~> 1.0.0) |
|
73 | 72 |
daemons (1.1.9) |
74 | 73 |
debug_inspector (0.0.2) |
75 |
- delayed_job (4.0.0) |
|
76 |
- activesupport (>= 3.0, < 4.1) |
|
77 |
- delayed_job_active_record (4.0.0) |
|
78 |
- activerecord (>= 3.0, < 4.1) |
|
74 |
+ delayed_job (4.0.1) |
|
75 |
+ activesupport (>= 3.0, < 4.2) |
|
76 |
+ delayed_job_active_record (4.0.1) |
|
77 |
+ activerecord (>= 3.0, < 4.2) |
|
79 | 78 |
delayed_job (>= 3.0, < 4.1) |
80 |
- devise (3.0.0) |
|
81 |
- bcrypt-ruby (~> 3.0) |
|
79 |
+ delorean (2.1.0) |
|
80 |
+ chronic |
|
81 |
+ devise (3.2.4) |
|
82 |
+ bcrypt (~> 3.0) |
|
82 | 83 |
orm_adapter (~> 0.1) |
83 | 84 |
railties (>= 3.2.6, < 5) |
85 |
+ thread_safe (~> 0.1) |
|
84 | 86 |
warden (~> 1.2.3) |
85 |
- diff-lcs (1.2.4) |
|
86 |
- docile (1.1.1) |
|
87 |
- dotenv (0.9.0) |
|
88 |
- dotenv-rails (0.9.0) |
|
89 |
- dotenv (= 0.9.0) |
|
87 |
+ diff-lcs (1.2.5) |
|
88 |
+ docile (1.1.3) |
|
89 |
+ dotenv (0.11.1) |
|
90 |
+ dotenv-deployment (~> 0.0.2) |
|
91 |
+ dotenv-deployment (0.0.2) |
|
92 |
+ dotenv-rails (0.11.1) |
|
93 |
+ dotenv (= 0.11.1) |
|
90 | 94 |
em-http-request (1.1.2) |
91 | 95 |
addressable (>= 2.3.4) |
92 | 96 |
cookiejar |
@@ -96,19 +100,18 @@ GEM |
||
96 | 100 |
em-socksify (0.3.0) |
97 | 101 |
eventmachine (>= 1.0.0.beta.4) |
98 | 102 |
equalizer (0.0.9) |
99 |
- erector (0.9.0) |
|
103 |
+ erector (0.10.0) |
|
100 | 104 |
treetop (>= 1.2.3) |
101 | 105 |
erubis (2.7.0) |
102 |
- ethon (0.5.12) |
|
106 |
+ ethon (0.7.0) |
|
103 | 107 |
ffi (>= 1.3.0) |
104 |
- mime-types (~> 1.18) |
|
105 | 108 |
eventmachine (1.0.3) |
106 |
- execjs (1.4.0) |
|
107 |
- multi_json (~> 1.0) |
|
109 |
+ execjs (2.0.2) |
|
108 | 110 |
faraday (0.9.0) |
109 | 111 |
multipart-post (>= 1.2, < 3) |
110 |
- fastercsv (1.5.5) |
|
111 |
- ffi (1.9.0) |
|
112 |
+ faraday_middleware (0.9.1) |
|
113 |
+ faraday (>= 0.7.4, < 0.10) |
|
114 |
+ ffi (1.9.3) |
|
112 | 115 |
forecast_io (2.0.0) |
113 | 116 |
faraday |
114 | 117 |
hashie |
@@ -116,90 +119,86 @@ GEM |
||
116 | 119 |
foreman (0.63.0) |
117 | 120 |
dotenv (>= 0.7) |
118 | 121 |
thor (>= 0.13.6) |
119 |
- geokit (1.6.5) |
|
120 |
- multi_json |
|
121 |
- geokit-rails3 (0.1.5) |
|
122 |
+ geokit (1.8.4) |
|
123 |
+ multi_json (>= 1.3.2) |
|
124 |
+ geokit-rails (2.0.1) |
|
122 | 125 |
geokit (~> 1.5) |
123 |
- rails (~> 3.0) |
|
126 |
+ rails (>= 3.0) |
|
124 | 127 |
hashie (2.0.5) |
125 | 128 |
hike (1.2.3) |
129 |
+ hipchat (1.1.0) |
|
130 |
+ httparty |
|
126 | 131 |
http (0.5.0) |
127 | 132 |
http_parser.rb |
128 | 133 |
http_parser.rb (0.6.0) |
129 |
- httparty (0.11.0) |
|
130 |
- multi_json (~> 1.0) |
|
134 |
+ httparty (0.13.1) |
|
135 |
+ json (~> 1.8) |
|
131 | 136 |
multi_xml (>= 0.5.2) |
132 |
- httpauth (0.2.0) |
|
133 |
- i18n (0.6.1) |
|
134 |
- journey (1.0.4) |
|
135 |
- jquery-rails (3.0.4) |
|
137 |
+ i18n (0.6.9) |
|
138 |
+ jquery-rails (3.1.0) |
|
136 | 139 |
railties (>= 3.0, < 5.0) |
137 | 140 |
thor (>= 0.14, < 2.0) |
138 | 141 |
json (1.8.1) |
139 |
- jsonpath (0.5.3) |
|
142 |
+ jsonpath (0.5.6) |
|
140 | 143 |
multi_json |
141 |
- jwt (0.1.8) |
|
144 |
+ jwt (0.1.11) |
|
142 | 145 |
multi_json (>= 1.5) |
143 |
- kaminari (0.14.1) |
|
146 |
+ kaminari (0.15.1) |
|
144 | 147 |
actionpack (>= 3.0.0) |
145 | 148 |
activesupport (>= 3.0.0) |
146 |
- kramdown (1.1.0) |
|
149 |
+ kramdown (1.3.3) |
|
147 | 150 |
libv8 (3.16.14.3) |
148 |
- macaddr (1.6.7) |
|
151 |
+ macaddr (1.7.1) |
|
149 | 152 |
systemu (~> 2.6.2) |
150 | 153 |
mail (2.5.4) |
151 | 154 |
mime-types (~> 1.16) |
152 | 155 |
treetop (~> 1.4.8) |
153 |
- memoizable (0.4.0) |
|
154 |
- thread_safe (~> 0.1.3) |
|
155 |
- method_source (0.8.1) |
|
156 |
- mime-types (1.24) |
|
157 |
- mini_portile (0.5.1) |
|
158 |
- multi_json (1.7.9) |
|
156 |
+ memoizable (0.4.2) |
|
157 |
+ thread_safe (~> 0.3, >= 0.3.1) |
|
158 |
+ method_source (0.8.2) |
|
159 |
+ mime-types (1.25.1) |
|
160 |
+ mini_portile (0.5.3) |
|
161 |
+ minitest (5.3.3) |
|
162 |
+ multi_json (1.9.3) |
|
159 | 163 |
multi_xml (0.5.5) |
160 | 164 |
multipart-post (2.0.0) |
161 |
- mysql2 (0.3.13) |
|
165 |
+ mysql2 (0.3.15) |
|
162 | 166 |
naught (1.0.0) |
163 |
- nokogiri (1.6.0) |
|
167 |
+ nokogiri (1.6.1) |
|
164 | 168 |
mini_portile (~> 0.5.0) |
165 |
- oauth2 (0.9.2) |
|
166 |
- faraday (~> 0.8) |
|
167 |
- httpauth (~> 0.2) |
|
168 |
- jwt (~> 0.1.4) |
|
169 |
- multi_json (~> 1.0) |
|
169 |
+ oauth2 (0.9.3) |
|
170 |
+ faraday (>= 0.8, < 0.10) |
|
171 |
+ jwt (~> 0.1.8) |
|
172 |
+ multi_json (~> 1.3) |
|
170 | 173 |
multi_xml (~> 0.5) |
171 | 174 |
rack (~> 1.2) |
172 |
- orm_adapter (0.4.0) |
|
173 |
- polyglot (0.3.3) |
|
174 |
- pry (0.9.12.2) |
|
175 |
- coderay (~> 1.0.5) |
|
175 |
+ orm_adapter (0.5.0) |
|
176 |
+ polyglot (0.3.4) |
|
177 |
+ protected_attributes (1.0.7) |
|
178 |
+ activemodel (>= 4.0.1, < 5.0) |
|
179 |
+ pry (0.9.12.6) |
|
180 |
+ coderay (~> 1.0) |
|
176 | 181 |
method_source (~> 0.8) |
177 | 182 |
slop (~> 3.4) |
178 |
- rack (1.4.5) |
|
179 |
- rack-cache (1.2) |
|
180 |
- rack (>= 0.4) |
|
181 |
- rack-ssl (1.3.3) |
|
182 |
- rack |
|
183 |
+ rack (1.5.2) |
|
183 | 184 |
rack-test (0.6.2) |
184 | 185 |
rack (>= 1.0) |
185 |
- rails (3.2.13) |
|
186 |
- actionmailer (= 3.2.13) |
|
187 |
- actionpack (= 3.2.13) |
|
188 |
- activerecord (= 3.2.13) |
|
189 |
- activeresource (= 3.2.13) |
|
190 |
- activesupport (= 3.2.13) |
|
191 |
- bundler (~> 1.0) |
|
192 |
- railties (= 3.2.13) |
|
193 |
- railties (3.2.13) |
|
194 |
- actionpack (= 3.2.13) |
|
195 |
- activesupport (= 3.2.13) |
|
196 |
- rack-ssl (~> 1.3.2) |
|
186 |
+ rails (4.1.0) |
|
187 |
+ actionmailer (= 4.1.0) |
|
188 |
+ actionpack (= 4.1.0) |
|
189 |
+ actionview (= 4.1.0) |
|
190 |
+ activemodel (= 4.1.0) |
|
191 |
+ activerecord (= 4.1.0) |
|
192 |
+ activesupport (= 4.1.0) |
|
193 |
+ bundler (>= 1.3.0, < 2.0) |
|
194 |
+ railties (= 4.1.0) |
|
195 |
+ sprockets-rails (~> 2.0) |
|
196 |
+ railties (4.1.0) |
|
197 |
+ actionpack (= 4.1.0) |
|
198 |
+ activesupport (= 4.1.0) |
|
197 | 199 |
rake (>= 0.8.7) |
198 |
- rdoc (~> 3.4) |
|
199 |
- thor (>= 0.14.6, < 2.0) |
|
200 |
- rake (10.1.0) |
|
201 |
- rdoc (3.12.2) |
|
202 |
- json (~> 1.4) |
|
200 |
+ thor (>= 0.18.1, < 2.0) |
|
201 |
+ rake (10.3.1) |
|
203 | 202 |
ref (1.0.5) |
204 | 203 |
rest-client (1.6.7) |
205 | 204 |
mime-types (>= 1.16) |
@@ -208,35 +207,36 @@ GEM |
||
208 | 207 |
rspec-core (~> 2.14.0) |
209 | 208 |
rspec-expectations (~> 2.14.0) |
210 | 209 |
rspec-mocks (~> 2.14.0) |
211 |
- rspec-core (2.14.5) |
|
212 |
- rspec-expectations (2.14.2) |
|
210 |
+ rspec-core (2.14.8) |
|
211 |
+ rspec-expectations (2.14.5) |
|
213 | 212 |
diff-lcs (>= 1.1.3, < 2.0) |
214 |
- rspec-mocks (2.14.3) |
|
215 |
- rspec-rails (2.14.0) |
|
213 |
+ rspec-mocks (2.14.6) |
|
214 |
+ rspec-rails (2.14.2) |
|
216 | 215 |
actionpack (>= 3.0) |
216 |
+ activemodel (>= 3.0) |
|
217 | 217 |
activesupport (>= 3.0) |
218 | 218 |
railties (>= 3.0) |
219 | 219 |
rspec-core (~> 2.14.0) |
220 | 220 |
rspec-expectations (~> 2.14.0) |
221 | 221 |
rspec-mocks (~> 2.14.0) |
222 |
- rturk (2.11.0) |
|
222 |
+ rturk (2.12.1) |
|
223 | 223 |
erector |
224 | 224 |
nokogiri |
225 | 225 |
rest-client |
226 | 226 |
ruby-growl (4.1) |
227 | 227 |
uuid (~> 2.3, >= 2.3.5) |
228 |
- rufus-scheduler (3.0.2) |
|
228 |
+ rufus-scheduler (3.0.7) |
|
229 | 229 |
tzinfo |
230 |
- safe_yaml (0.9.5) |
|
231 |
- sass (3.2.9) |
|
232 |
- sass-rails (3.2.6) |
|
233 |
- railties (~> 3.2.0) |
|
234 |
- sass (>= 3.1.10) |
|
235 |
- tilt (~> 1.3) |
|
236 |
- select2-rails (3.4.3) |
|
237 |
- sass-rails |
|
230 |
+ safe_yaml (1.0.3) |
|
231 |
+ sass (3.2.19) |
|
232 |
+ sass-rails (4.0.3) |
|
233 |
+ railties (>= 4.0.0, < 5.0) |
|
234 |
+ sass (~> 3.2.0) |
|
235 |
+ sprockets (~> 2.8, <= 2.11.0) |
|
236 |
+ sprockets-rails (~> 2.0) |
|
237 |
+ select2-rails (3.5.4) |
|
238 | 238 |
thor (~> 0.14) |
239 |
- shoulda-matchers (2.2.0) |
|
239 |
+ shoulda-matchers (2.6.0) |
|
240 | 240 |
activesupport (>= 3.0.0) |
241 | 241 |
simple_oauth (0.2.0) |
242 | 242 |
simplecov (0.8.2) |
@@ -244,32 +244,34 @@ GEM |
||
244 | 244 |
multi_json |
245 | 245 |
simplecov-html (~> 0.8.0) |
246 | 246 |
simplecov-html (0.8.0) |
247 |
- slop (3.4.5) |
|
248 |
- sprockets (2.2.2) |
|
247 |
+ slop (3.5.0) |
|
248 |
+ sprockets (2.11.0) |
|
249 | 249 |
hike (~> 1.2) |
250 | 250 |
multi_json (~> 1.0) |
251 | 251 |
rack (~> 1.0) |
252 | 252 |
tilt (~> 1.1, != 1.3.0) |
253 |
- system_timer (1.2.4) |
|
253 |
+ sprockets-rails (2.1.3) |
|
254 |
+ actionpack (>= 3.0) |
|
255 |
+ activesupport (>= 3.0) |
|
256 |
+ sprockets (~> 2.8) |
|
254 | 257 |
systemu (2.6.4) |
255 |
- term-ansicolor (1.2.2) |
|
256 |
- tins (~> 0.8) |
|
257 |
- therubyracer (0.12.0) |
|
258 |
+ term-ansicolor (1.3.0) |
|
259 |
+ tins (~> 1.0) |
|
260 |
+ therubyracer (0.12.1) |
|
258 | 261 |
libv8 (~> 3.16.14.0) |
259 | 262 |
ref |
260 |
- thor (0.18.1) |
|
261 |
- thread_safe (0.1.3) |
|
262 |
- atomic |
|
263 |
+ thor (0.19.1) |
|
264 |
+ thread_safe (0.3.3) |
|
263 | 265 |
tilt (1.4.1) |
264 |
- tins (0.13.1) |
|
266 |
+ tins (1.1.0) |
|
265 | 267 |
treetop (1.4.15) |
266 | 268 |
polyglot |
267 | 269 |
polyglot (>= 0.3.1) |
268 |
- twilio-ruby (3.10.0) |
|
270 |
+ twilio-ruby (3.11.5) |
|
269 | 271 |
builder (>= 2.1.2) |
270 | 272 |
jwt (>= 0.1.2) |
271 | 273 |
multi_json (>= 1.3.0) |
272 |
- twitter (5.7.1) |
|
274 |
+ twitter (5.8.0) |
|
273 | 275 |
addressable (~> 2.3) |
274 | 276 |
buftok (~> 0.2.0) |
275 | 277 |
equalizer (~> 0.0.9) |
@@ -280,25 +282,26 @@ GEM |
||
280 | 282 |
memoizable (~> 0.4.0) |
281 | 283 |
naught (~> 1.0) |
282 | 284 |
simple_oauth (~> 0.2.0) |
283 |
- typhoeus (0.6.3) |
|
284 |
- ethon (~> 0.5.11) |
|
285 |
- tzinfo (0.3.38) |
|
286 |
- uglifier (2.1.2) |
|
285 |
+ typhoeus (0.6.8) |
|
286 |
+ ethon (>= 0.7.0) |
|
287 |
+ tzinfo (1.1.0) |
|
288 |
+ thread_safe (~> 0.1) |
|
289 |
+ uglifier (2.5.0) |
|
287 | 290 |
execjs (>= 0.3.0) |
288 |
- multi_json (~> 1.0, >= 1.0.2) |
|
291 |
+ json (>= 1.8.0) |
|
289 | 292 |
uuid (2.3.7) |
290 | 293 |
macaddr (~> 1.0) |
291 | 294 |
warden (1.2.3) |
292 | 295 |
rack (>= 1.0) |
293 |
- webmock (1.13.0) |
|
296 |
+ webmock (1.17.4) |
|
294 | 297 |
addressable (>= 2.2.7) |
295 | 298 |
crack (>= 0.3.2) |
296 |
- weibo_2 (0.1.4) |
|
299 |
+ weibo_2 (0.1.6) |
|
297 | 300 |
hashie (~> 2.0.4) |
298 |
- multi_json (~> 1.7.2) |
|
301 |
+ multi_json (~> 1) |
|
299 | 302 |
oauth2 (~> 0.9.1) |
300 | 303 |
rest-client (~> 1.6.7) |
301 |
- wunderground (1.1.0) |
|
304 |
+ wunderground (1.2.0) |
|
302 | 305 |
addressable |
303 | 306 |
httparty (> 0.6.0) |
304 | 307 |
json (> 1.4.0) |
@@ -308,49 +311,55 @@ PLATFORMS |
||
308 | 311 |
ruby |
309 | 312 |
|
310 | 313 |
DEPENDENCIES |
311 |
- ace-rails-ap |
|
314 |
+ ace-rails-ap (~> 2.0.1) |
|
312 | 315 |
better_errors |
313 | 316 |
binding_of_caller |
314 |
- bootstrap-kaminari-views |
|
315 |
- coffee-rails (~> 3.2.1) |
|
317 |
+ bootstrap-kaminari-views (~> 0.0.2) |
|
318 |
+ coffee-rails (~> 4.0.0) |
|
316 | 319 |
coveralls |
317 |
- daemons |
|
318 |
- delayed_job |
|
319 |
- delayed_job_active_record |
|
320 |
- devise |
|
320 |
+ daemons (~> 1.1.9) |
|
321 |
+ delayed_job (~> 4.0.0) |
|
322 |
+ delayed_job_active_record (~> 4.0.0) |
|
323 |
+ delorean |
|
324 |
+ devise (~> 3.2.4) |
|
325 |
+ dotenv-deployment |
|
321 | 326 |
dotenv-rails |
322 |
- em-http-request |
|
323 |
- fastercsv |
|
324 |
- forecast_io |
|
325 |
- foreman |
|
326 |
- geokit-rails3 |
|
327 |
- jquery-rails |
|
328 |
- json (>= 1.7.7) |
|
329 |
- jsonpath |
|
330 |
- kaminari |
|
331 |
- kramdown |
|
332 |
- mysql2 |
|
333 |
- nokogiri |
|
327 |
+ em-http-request (~> 1.1.2) |
|
328 |
+ faraday (~> 0.9.0) |
|
329 |
+ faraday_middleware |
|
330 |
+ forecast_io (~> 2.0.0) |
|
331 |
+ foreman (~> 0.63.0) |
|
332 |
+ geokit (~> 1.8.4) |
|
333 |
+ geokit-rails (~> 2.0.1) |
|
334 |
+ hipchat (~> 1.1.0) |
|
335 |
+ jquery-rails (~> 3.1.0) |
|
336 |
+ json (~> 1.8.1) |
|
337 |
+ jsonpath (~> 0.5.3) |
|
338 |
+ kaminari (~> 0.15.1) |
|
339 |
+ kramdown (~> 1.3.3) |
|
340 |
+ mysql2 (~> 0.3.15) |
|
341 |
+ nokogiri (~> 1.6.1) |
|
342 |
+ protected_attributes (~> 1.0.7) |
|
334 | 343 |
pry |
335 |
- rails |
|
336 |
- rake |
|
344 |
+ rack |
|
345 |
+ rails (= 4.1.0) |
|
337 | 346 |
rr |
338 | 347 |
rspec |
339 | 348 |
rspec-rails |
340 |
- rturk |
|
341 |
- ruby-growl |
|
342 |
- rufus-scheduler |
|
343 |
- sass-rails (~> 3.2.3) |
|
344 |
- select2-rails |
|
349 |
+ rturk (~> 2.12.1) |
|
350 |
+ ruby-growl (~> 4.1.0) |
|
351 |
+ rufus-scheduler (~> 3.0.7) |
|
352 |
+ sass-rails (~> 4.0.0) |
|
353 |
+ select2-rails (~> 3.5.4) |
|
345 | 354 |
shoulda-matchers |
346 |
- system_timer |
|
347 |
- therubyracer |
|
348 |
- twilio-ruby |
|
349 |
- twitter (~> 5.7.1) |
|
355 |
+ therubyracer (~> 0.12.1) |
|
356 |
+ twilio-ruby (~> 3.11.5) |
|
357 |
+ twitter (~> 5.8.0) |
|
350 | 358 |
twitter-stream! |
351 |
- typhoeus |
|
352 |
- uglifier (>= 1.0.3) |
|
359 |
+ typhoeus (~> 0.6.3) |
|
360 |
+ tzinfo-data |
|
361 |
+ uglifier (>= 1.3.0) |
|
353 | 362 |
webmock |
354 |
- weibo_2 |
|
355 |
- wunderground |
|
363 |
+ weibo_2 (~> 0.1.4) |
|
364 |
+ wunderground (~> 1.2.0) |
|
356 | 365 |
xmpp4r (~> 0.5.6) |
@@ -8,4 +8,4 @@ dj: bundle exec script/delayed_job run |
||
8 | 8 |
# web: bundle exec unicorn -c config/unicorn/production.rb |
9 | 9 |
# schedule: bundle exec rails runner bin/schedule.rb |
10 | 10 |
# twitter: bundle exec rails runner bin/twitter_stream.rb |
11 |
-# dj: bundle exec script/delayed_job run |
|
11 |
+# dj: bundle exec script/delayed_job run |
@@ -24,7 +24,7 @@ Follow [@tectonic](https://twitter.com/tectonic) for updates as Huginn evolves, |
||
24 | 24 |
|
25 | 25 |
### We need your help! |
26 | 26 |
|
27 |
-Want to help with Huginn? Try tackling [issues tagged with #help-wanted](https://github.com/cantino/huginn/issues?direction=desc&labels=help-wanted&page=1&sort=created&state=open). |
|
27 |
+Want to help with Huginn? All contributions are encouraged! You could make UI improvements, add new Agents, write documentation and tutorials, or try tackling [issues tagged with #help-wanted](https://github.com/cantino/huginn/issues?direction=desc&labels=help-wanted&page=1&sort=created&state=open). |
|
28 | 28 |
|
29 | 29 |
## Examples |
30 | 30 |
|
@@ -49,44 +49,29 @@ And now, some example screenshots. Below them are instructions to get you start |
||
49 | 49 |
If you just want to play around, you can simply clone this repository, then perform the following steps: |
50 | 50 |
|
51 | 51 |
* Copy `.env.example` to `.env` (`cp .env.example .env`) and edit `.env`, at least updating the `APP_SECRET_TOKEN` variable. |
52 |
-* Run `rake db:create`, `rake db:migrate`, and then `rake db:seed` to create a development MySQL database with some example seed data. |
|
52 |
+* Run `rake db:create`, `rake db:migrate`, and then `rake db:seed` to create a development MySQL database with some example Agents. |
|
53 | 53 |
* Run `foreman start`, visit [http://localhost:3000/][localhost], and login with the username of `admin` and the password of `password`. |
54 | 54 |
* Setup some Agents! |
55 |
+* Read the [wiki][wiki] for usage examples and to get started making new Agents. |
|
55 | 56 |
|
56 | 57 |
Note: by default, emails are not sent in the `development` Rails environment, which is what you just setup. If you'd like to enable emails when playing with Huginn locally, edit `config.action_mailer.perform_deliveries` in `config/environments/development.rb`. |
57 | 58 |
|
58 | 59 |
If you need more detailed instructions, see the [Novice setup guide][novice-setup-guide]. |
59 | 60 |
|
60 | 61 |
[localhost]: http://localhost:3000/ |
62 |
+[wiki]: https://github.com/cantino/huginn/wiki |
|
61 | 63 |
[novice-setup-guide]: https://github.com/cantino/huginn/wiki/Novice-setup-guide |
62 | 64 |
|
63 |
-### Real Start |
|
64 |
- |
|
65 |
-Follow these instructions if you wish to deploy your own version of Huginn or contribute back to the project. Github doesn't make it easy to work with private forks of public repositories, so I recommend that you follow the following steps: |
|
66 |
- |
|
67 |
-* Make a public fork of Huginn. If you can't create private Github repositories, you can skip the steps below. Just follow the *Quick Start* steps above and make pull requests when you want to contribute a patch. |
|
68 |
-* Make a private, empty Github repository called `huginn-private` |
|
69 |
-* Duplicate your public fork into your new private repository (via [Github's instructions](https://help.github.com/articles/duplicating-a-repository)): |
|
70 |
- |
|
71 |
- git clone --bare git@github.com:you/huginn.git |
|
72 |
- cd huginn.git |
|
73 |
- git push --mirror git@github.com:you/huginn-private.git |
|
74 |
- cd .. && rm -rf huginn.git |
|
75 |
- |
|
76 |
-* Checkout your new private repository. |
|
77 |
-* Add your Huginn public fork as a remote to your new private repository (`huginn-private`): |
|
78 |
- |
|
79 |
- git remote add public git@github.com:you/huginn.git |
|
80 |
- |
|
81 |
-* Run the steps from *Quick Start* above to configure your copy of Huginn. |
|
82 |
-* When you want to contribute patches, do a remote push from your private repository to your public fork of the relevant commits, then make a pull request to this repository. |
|
83 |
- |
|
84 | 65 |
## Deployment |
85 | 66 |
|
86 | 67 |
Please see [the Huginn Wiki](https://github.com/cantino/huginn/wiki#deploying-huginn) for detailed deployment strategies for different providers. |
87 | 68 |
|
88 | 69 |
### Optional Setup |
89 | 70 |
|
71 |
+#### Setup for private development |
|
72 |
+ |
|
73 |
+See [private development instructions](https://github.com/cantino/huginn/wiki/Private-development-instructions) on the wiki. |
|
74 |
+ |
|
90 | 75 |
#### Enable the WeatherAgent |
91 | 76 |
|
92 | 77 |
In order to use the WeatherAgent you need an [API key with Wunderground](http://www.wunderground.com/weather/api/). Signup for one and then change value of `api_key: your-key` in your seeded WeatherAgent. |
@@ -119,5 +104,5 @@ Huginn is a work in progress and is hopefully just getting started. Please get |
||
119 | 104 |
|
120 | 105 |
Please fork, add specs, and send pull requests! |
121 | 106 |
|
122 |
-[](https://travis-ci.org/cantino/huginn) [](https://coveralls.io/r/cantino/huginn) [](https://bitdeli.com/free "Bitdeli Badge") |
|
107 |
+[](https://travis-ci.org/cantino/huginn) [](https://coveralls.io/r/cantino/huginn) [](https://bitdeli.com/free "Bitdeli Badge") [](https://gemnasium.com/cantino/huginn) |
|
123 | 108 |
|
@@ -56,9 +56,6 @@ $(document).ready -> |
||
56 | 56 |
# JSON Editor |
57 | 57 |
window.jsonEditor = setupJsonEditor() |
58 | 58 |
|
59 |
- # Select2 Selects |
|
60 |
- $(".select2").select2(width: 'resolve') |
|
61 |
- |
|
62 | 59 |
# Flash |
63 | 60 |
if $(".flash").length |
64 | 61 |
setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000) |
@@ -155,6 +152,9 @@ $(document).ready -> |
||
155 | 152 |
|
156 | 153 |
$("#agent_type").change() if $("#agent_type").length |
157 | 154 |
|
155 |
+ # Select2 Selects |
|
156 |
+ $(".select2").select2(width: 'resolve') |
|
157 |
+ |
|
158 | 158 |
if $(".schedule-region") |
159 | 159 |
if $(".schedule-region").data("can-be-scheduled") == true |
160 | 160 |
showSchedule() |
@@ -0,0 +1,39 @@ |
||
1 |
+module JsonPathOptionsOverwritable |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ # Using this concern allows providing optional `<attribute>_path` options hash |
|
4 |
+ # attributes which will then (if not blank) be interpolated using the provided JSONPath. |
|
5 |
+ # |
|
6 |
+ # Example options Hash: |
|
7 |
+ # { |
|
8 |
+ # name: 'Huginn', |
|
9 |
+ # name_path: '$.name', |
|
10 |
+ # title: 'Hello from Huginn' |
|
11 |
+ # title_path: '' |
|
12 |
+ # } |
|
13 |
+ # Example event payload: |
|
14 |
+ # { |
|
15 |
+ # name: 'dynamic huginn' |
|
16 |
+ # } |
|
17 |
+ # calling agent.merge_json_path_options(event) returns the following hash: |
|
18 |
+ # { |
|
19 |
+ # name: 'dynamic huginn' |
|
20 |
+ # title: 'Hello from Huginn' |
|
21 |
+ # } |
|
22 |
+ |
|
23 |
+ private |
|
24 |
+ def merge_json_path_options(event) |
|
25 |
+ options.select { |k, v| options_with_path.include? k}.tap do |merged_options| |
|
26 |
+ options_with_path.each do |a| |
|
27 |
+ merged_options[a] = select_option(event, a) |
|
28 |
+ end |
|
29 |
+ end |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ def select_option(event, a) |
|
33 |
+ if options[a.to_s + '_path'].present? |
|
34 |
+ Utils.value_at(event.payload, options[a.to_s + '_path']) |
|
35 |
+ else |
|
36 |
+ options[a] |
|
37 |
+ end |
|
38 |
+ end |
|
39 |
+end |
@@ -0,0 +1,15 @@ |
||
1 |
+module WorkingHelpers |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ def event_created_within?(days) |
|
5 |
+ last_event_at && last_event_at > days.to_i.days.ago |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ def recent_error_logs? |
|
9 |
+ last_event_at && last_error_log_at && last_error_log_at > (last_event_at - 2.minutes) |
|
10 |
+ end |
|
11 |
+ |
|
12 |
+ def received_event_without_error? |
|
13 |
+ (last_receive_at.present? && last_error_log_at.blank?) || (last_receive_at.present? && last_error_log_at.present? && last_receive_at > last_error_log_at) |
|
14 |
+ end |
|
15 |
+end |
@@ -1,4 +1,6 @@ |
||
1 | 1 |
class AgentsController < ApplicationController |
2 |
+ include DotHelper |
|
3 |
+ |
|
2 | 4 |
def index |
3 | 5 |
@agents = current_user.agents.page(params[:page]) |
4 | 6 |
|
@@ -69,7 +71,13 @@ class AgentsController < ApplicationController |
||
69 | 71 |
end |
70 | 72 |
|
71 | 73 |
def new |
72 |
- @agent = current_user.agents.build |
|
74 |
+ agents = current_user.agents |
|
75 |
+ |
|
76 |
+ if id = params[:id] |
|
77 |
+ @agent = agents.build_clone(agents.find(id)) |
|
78 |
+ else |
|
79 |
+ @agent = agents.build |
|
80 |
+ end |
|
73 | 81 |
|
74 | 82 |
respond_to do |format| |
75 | 83 |
format.html |
@@ -0,0 +1,40 @@ |
||
1 |
+module DotHelper |
|
2 |
+ def render_agents_diagram(agents) |
|
3 |
+ if (command = ENV['USE_GRAPHVIZ_DOT']) && |
|
4 |
+ (svg = IO.popen([command, *%w[-Tsvg -q1 -o/dev/stdout /dev/stdin]], 'w+') { |dot| |
|
5 |
+ dot.print agents_dot(agents, true) |
|
6 |
+ dot.close_write |
|
7 |
+ dot.read |
|
8 |
+ } rescue false) |
|
9 |
+ svg.html_safe |
|
10 |
+ else |
|
11 |
+ tag('img', src: URI('https://chart.googleapis.com/chart').tap { |uri| |
|
12 |
+ uri.query = URI.encode_www_form(cht: 'gv', chl: agents_dot(agents)) |
|
13 |
+ }) |
|
14 |
+ end |
|
15 |
+ end |
|
16 |
+ |
|
17 |
+ private |
|
18 |
+ |
|
19 |
+ def dot_id(string) |
|
20 |
+ # Backslash escaping seems to work for the backslash itself, |
|
21 |
+ # despite the DOT language document. |
|
22 |
+ '"%s"' % string.gsub(/\\/, "\\\\\\\\").gsub(/"/, "\\\\\"") |
|
23 |
+ end |
|
24 |
+ |
|
25 |
+ def agents_dot(agents, rich = false) |
|
26 |
+ "digraph foo {".tap { |dot| |
|
27 |
+ agents.each.with_index do |agent, index| |
|
28 |
+ if rich |
|
29 |
+ dot << '%s[URL=%s];' % [dot_id(agent.name), dot_id(agent_path(agent.id))] |
|
30 |
+ else |
|
31 |
+ dot << '%s;' % dot_id(agent.name) |
|
32 |
+ end |
|
33 |
+ agent.receivers.each do |receiver| |
|
34 |
+ dot << "%s->%s;" % [dot_id(agent.name), dot_id(receiver.name)] |
|
35 |
+ end |
|
36 |
+ end |
|
37 |
+ dot << "}" |
|
38 |
+ } |
|
39 |
+ end |
|
40 |
+end |
@@ -11,12 +11,13 @@ class Agent < ActiveRecord::Base |
||
11 | 11 |
include MarkdownClassAttributes |
12 | 12 |
include JSONSerializedField |
13 | 13 |
include RDBMSFunctions |
14 |
+ include WorkingHelpers |
|
14 | 15 |
|
15 | 16 |
markdown_class_attributes :description, :event_description |
16 | 17 |
|
17 | 18 |
load_types_in "Agents" |
18 | 19 |
|
19 |
- SCHEDULES = %w[every_2m every_5m every_10m every_30m every_1h every_2h every_5h every_12h every_1d every_2d every_7d |
|
20 |
+ SCHEDULES = %w[every_1m every_2m every_5m every_10m every_30m every_1h every_2h every_5h every_12h every_1d every_2d every_7d |
|
20 | 21 |
midnight 1am 2am 3am 4am 5am 6am 7am 8am 9am 10am 11am noon 1pm 2pm 3pm 4pm 5pm 6pm 7pm 8pm 9pm 10pm 11pm never] |
21 | 22 |
|
22 | 23 |
EVENT_RETENTION_SCHEDULES = [["Forever", 0], ["1 day", 1], *([2, 3, 4, 5, 7, 14, 21, 30, 45, 90, 180, 365].map {|n| ["#{n} days", n] })] |
@@ -39,10 +40,10 @@ class Agent < ActiveRecord::Base |
||
39 | 40 |
after_save :possibly_update_event_expirations |
40 | 41 |
|
41 | 42 |
belongs_to :user, :inverse_of => :agents |
42 |
- has_many :events, :dependent => :delete_all, :inverse_of => :agent, :order => "events.id desc" |
|
43 |
+ has_many :events, -> { order("events.id desc") }, :dependent => :delete_all, :inverse_of => :agent |
|
43 | 44 |
has_one :most_recent_event, :inverse_of => :agent, :class_name => "Event", :order => "events.id desc" |
44 |
- has_many :logs, :dependent => :delete_all, :inverse_of => :agent, :class_name => "AgentLog", :order => "agent_logs.id desc" |
|
45 |
- has_many :received_events, :through => :sources, :class_name => "Event", :source => :events, :order => "events.id desc" |
|
45 |
+ has_many :logs, -> { order("agent_logs.id desc") }, :dependent => :delete_all, :inverse_of => :agent, :class_name => "AgentLog" |
|
46 |
+ has_many :received_events, -> { order("events.id desc") }, :through => :sources, :class_name => "Event", :source => :events |
|
46 | 47 |
has_many :links_as_source, :dependent => :delete_all, :foreign_key => "source_id", :class_name => "Link", :inverse_of => :source |
47 | 48 |
has_many :links_as_receiver, :dependent => :delete_all, :foreign_key => "receiver_id", :class_name => "Link", :inverse_of => :receiver |
48 | 49 |
has_many :sources, :through => :links_as_receiver, :class_name => "Agent", :inverse_of => :receivers |
@@ -83,18 +84,6 @@ class Agent < ActiveRecord::Base |
||
83 | 84 |
raise "Implement me in your subclass" |
84 | 85 |
end |
85 | 86 |
|
86 |
- def validate_options |
|
87 |
- # Implement me in your subclass to test for valid options. |
|
88 |
- end |
|
89 |
- |
|
90 |
- def event_created_within?(days) |
|
91 |
- last_event_at && last_event_at > days.to_i.days.ago |
|
92 |
- end |
|
93 |
- |
|
94 |
- def recent_error_logs? |
|
95 |
- last_event_at && last_error_log_at && last_error_log_at > (last_event_at - 2.minutes) |
|
96 |
- end |
|
97 |
- |
|
98 | 87 |
def create_event(attrs) |
99 | 88 |
if can_create_events? |
100 | 89 |
events.create!({ |
@@ -193,17 +182,7 @@ class Agent < ActiveRecord::Base |
||
193 | 182 |
update_column :last_error_log_at, nil |
194 | 183 |
end |
195 | 184 |
|
196 |
- # Validations and Callbacks |
|
197 |
- |
|
198 |
- def sources_are_owned |
|
199 |
- errors.add(:sources, "must be owned by you") unless sources.all? {|s| s.user == user } |
|
200 |
- end |
|
201 |
- |
|
202 |
- def validate_schedule |
|
203 |
- unless cannot_be_scheduled? |
|
204 |
- errors.add(:schedule, "is not a valid schedule") unless SCHEDULES.include?(schedule.to_s) |
|
205 |
- end |
|
206 |
- end |
|
185 |
+ # Callbacks |
|
207 | 186 |
|
208 | 187 |
def set_default_schedule |
209 | 188 |
self.schedule = default_schedule unless schedule.present? || cannot_be_scheduled? |
@@ -222,10 +201,41 @@ class Agent < ActiveRecord::Base |
||
222 | 201 |
def possibly_update_event_expirations |
223 | 202 |
update_event_expirations! if keep_events_for_changed? |
224 | 203 |
end |
204 |
+ |
|
205 |
+ #Validation Methods |
|
206 |
+ |
|
207 |
+ private |
|
208 |
+ |
|
209 |
+ def sources_are_owned |
|
210 |
+ errors.add(:sources, "must be owned by you") unless sources.all? {|s| s.user == user } |
|
211 |
+ end |
|
212 |
+ |
|
213 |
+ def validate_schedule |
|
214 |
+ unless cannot_be_scheduled? |
|
215 |
+ errors.add(:schedule, "is not a valid schedule") unless SCHEDULES.include?(schedule.to_s) |
|
216 |
+ end |
|
217 |
+ end |
|
218 |
+ |
|
219 |
+ def validate_options |
|
220 |
+ # Implement me in your subclass to test for valid options. |
|
221 |
+ end |
|
225 | 222 |
|
226 | 223 |
# Class Methods |
227 | 224 |
|
228 | 225 |
class << self |
226 |
+ def build_clone(original) |
|
227 |
+ new(original.slice(:type, :options, :schedule, :source_ids, :keep_events_for, :propagate_immediately)) { |clone| |
|
228 |
+ # Give it a unique name |
|
229 |
+ 2.upto(count) do |i| |
|
230 |
+ name = '%s (%d)' % [original.name, i] |
|
231 |
+ unless exists?(name: name) |
|
232 |
+ clone.name = name |
|
233 |
+ break |
|
234 |
+ end |
|
235 |
+ end |
|
236 |
+ } |
|
237 |
+ end |
|
238 |
+ |
|
229 | 239 |
def cannot_be_scheduled! |
230 | 240 |
@cannot_be_scheduled = true |
231 | 241 |
end |
@@ -0,0 +1,90 @@ |
||
1 |
+module Agents |
|
2 |
+ class BasecampAgent < Agent |
|
3 |
+ cannot_receive_events! |
|
4 |
+ |
|
5 |
+ description <<-MD |
|
6 |
+ The BasecampAgent checks a Basecamp project for new Events |
|
7 |
+ |
|
8 |
+ It is required that you enter your Basecamp credentials (`username` and `password`). |
|
9 |
+ |
|
10 |
+ You also need to provide your Basecamp `user_id` and the `project_id` of the project you want to monitor. |
|
11 |
+ If you have your Basecamp project opened in your browser you can find the user_id and project_id as follows: |
|
12 |
+ |
|
13 |
+ `https://basecamp.com/` |
|
14 |
+ user_id |
|
15 |
+ `/projects/` |
|
16 |
+ project_id |
|
17 |
+ `-explore-basecamp` |
|
18 |
+ MD |
|
19 |
+ |
|
20 |
+ event_description <<-MD |
|
21 |
+ Events are the raw JSON provided by the Basecamp API. Should look something like: |
|
22 |
+ |
|
23 |
+ { |
|
24 |
+ "creator": { |
|
25 |
+ "fullsize_avatar_url": "https://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/original.gif?r=3", |
|
26 |
+ "avatar_url": "http://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/avatar.gif?r=3", |
|
27 |
+ "name": "Dominik Sander", |
|
28 |
+ "id": 123456 |
|
29 |
+ }, |
|
30 |
+ "attachments": [], |
|
31 |
+ "raw_excerpt": "test test", |
|
32 |
+ "excerpt": "test test", |
|
33 |
+ "id": 6454342343, |
|
34 |
+ "created_at": "2014-04-17T10:25:31.000+02:00", |
|
35 |
+ "updated_at": "2014-04-17T10:25:31.000+02:00", |
|
36 |
+ "summary": "commented on whaat", |
|
37 |
+ "action": "commented on", |
|
38 |
+ "target": "whaat", |
|
39 |
+ "url": "https://basecamp.com/12456/api/v1/projects/76454545-explore-basecamp/messages/76454545-whaat.json", |
|
40 |
+ "html_url": "https://basecamp.com/12456/projects/76454545-explore-basecamp/messages/76454545-whaat#comment_76454545" |
|
41 |
+ } |
|
42 |
+ MD |
|
43 |
+ |
|
44 |
+ default_schedule "every_10m" |
|
45 |
+ |
|
46 |
+ def default_options |
|
47 |
+ { |
|
48 |
+ 'username' => '', |
|
49 |
+ 'password' => '', |
|
50 |
+ 'user_id' => '', |
|
51 |
+ 'project_id' => '', |
|
52 |
+ } |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ def validate_options |
|
56 |
+ errors.add(:base, "you need to specify your basecamp username") unless options['username'].present? |
|
57 |
+ errors.add(:base, "you need to specify your basecamp password") unless options['password'].present? |
|
58 |
+ errors.add(:base, "you need to specify your basecamp user id") unless options['user_id'].present? |
|
59 |
+ errors.add(:base, "you need to specify the basecamp project id of which you want to receive events") unless options['project_id'].present? |
|
60 |
+ end |
|
61 |
+ |
|
62 |
+ def working? |
|
63 |
+ (events_count.present? && events_count > 0) |
|
64 |
+ end |
|
65 |
+ |
|
66 |
+ def check |
|
67 |
+ reponse = HTTParty.get request_url, request_options.merge(query_parameters) |
|
68 |
+ memory[:last_run] = Time.now.utc.iso8601 |
|
69 |
+ if last_check_at != nil |
|
70 |
+ JSON.parse(reponse.body).each do |event| |
|
71 |
+ create_event :payload => event |
|
72 |
+ end |
|
73 |
+ end |
|
74 |
+ save! |
|
75 |
+ end |
|
76 |
+ |
|
77 |
+ private |
|
78 |
+ def request_url |
|
79 |
+ "https://basecamp.com/#{URI.encode(options[:user_id].to_s)}/api/v1/projects/#{URI.encode(options[:project_id].to_s)}/events.json" |
|
80 |
+ end |
|
81 |
+ |
|
82 |
+ def request_options |
|
83 |
+ {:basic_auth => {:username =>options[:username], :password=>options[:password]}, :headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)"}} |
|
84 |
+ end |
|
85 |
+ |
|
86 |
+ def query_parameters |
|
87 |
+ memory[:last_run].present? ? { :query => {:since => memory[:last_run]} } : {} |
|
88 |
+ end |
|
89 |
+ end |
|
90 |
+end |
@@ -12,6 +12,10 @@ module Agents |
||
12 | 12 |
"celsius": "18", |
13 | 13 |
"fahreinheit": "64" |
14 | 14 |
}, |
15 |
+ "date": { |
|
16 |
+ "epoch": "1357959600", |
|
17 |
+ "pretty": "10:00 PM EST on January 11, 2013" |
|
18 |
+ }, |
|
15 | 19 |
"conditions": "Rain showers", |
16 | 20 |
"data": "This is some data" |
17 | 21 |
} |
@@ -33,6 +37,33 @@ module Agents |
||
33 | 37 |
"subject": "This is some data" |
34 | 38 |
} |
35 | 39 |
|
40 |
+ In `matchers` setting you can perform regular expression matching against contents of events and expand the match data for use in `instructions` setting. Here is an example: |
|
41 |
+ |
|
42 |
+ { |
|
43 |
+ "matchers": [ |
|
44 |
+ { |
|
45 |
+ "path": "$.date.pretty", |
|
46 |
+ "regexp": "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)", |
|
47 |
+ "to": "pretty_date", |
|
48 |
+ } |
|
49 |
+ ] |
|
50 |
+ } |
|
51 |
+ |
|
52 |
+ This virtually merges the following hash into the original event hash: |
|
53 |
+ |
|
54 |
+ "pretty_date": { |
|
55 |
+ "time": "10:00 PM EST", |
|
56 |
+ "0": "10:00 PM EST on January 11, 2013" |
|
57 |
+ "1": "10:00 PM EST", |
|
58 |
+ } |
|
59 |
+ |
|
60 |
+ So you can use it in `instructions` like this: |
|
61 |
+ |
|
62 |
+ "instructions": { |
|
63 |
+ "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius according to the forecast at <$.pretty_date.time>.", |
|
64 |
+ "subject": "$.data" |
|
65 |
+ } |
|
66 |
+ |
|
36 | 67 |
If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`. |
37 | 68 |
|
38 | 69 |
By default, the output event will have `agent` and `created_at` fields added as well, reflecting the original Agent type and Event creation time. You can skip these outputs by setting `skip_agent` and `skip_created_at` to `true`. |
@@ -46,8 +77,12 @@ module Agents |
||
46 | 77 |
|
47 | 78 |
event_description "User defined" |
48 | 79 |
|
80 |
+ after_save :clear_matchers |
|
81 |
+ |
|
49 | 82 |
def validate_options |
50 | 83 |
errors.add(:base, "instructions, mode, skip_agent, and skip_created_at all need to be present.") unless options['instructions'].present? and options['mode'].present? and options['skip_agent'].present? and options['skip_created_at'].present? |
84 |
+ |
|
85 |
+ validate_matchers |
|
51 | 86 |
end |
52 | 87 |
|
53 | 88 |
def default_options |
@@ -56,6 +91,7 @@ module Agents |
||
56 | 91 |
'message' => "You received a text <$.text> from <$.fields.from>", |
57 | 92 |
'some_other_field' => "Looks like the weather is going to be <$.fields.weather>" |
58 | 93 |
}, |
94 |
+ 'matchers' => [], |
|
59 | 95 |
'mode' => "clean", |
60 | 96 |
'skip_agent' => "false", |
61 | 97 |
'skip_created_at' => "false" |
@@ -68,12 +104,92 @@ module Agents |
||
68 | 104 |
|
69 | 105 |
def receive(incoming_events) |
70 | 106 |
incoming_events.each do |event| |
71 |
- formatted_event = options['mode'].to_s == "merge" ? event.payload : {} |
|
72 |
- options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, event.payload) } |
|
107 |
+ formatted_event = options['mode'].to_s == "merge" ? event.payload.dup : {} |
|
108 |
+ payload = perform_matching(event.payload) |
|
109 |
+ options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, payload) } |
|
73 | 110 |
formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless options['skip_agent'].to_s == "true" |
74 | 111 |
formatted_event['created_at'] = event.created_at unless options['skip_created_at'].to_s == "true" |
75 | 112 |
create_event :payload => formatted_event |
76 | 113 |
end |
77 | 114 |
end |
115 |
+ |
|
116 |
+ private |
|
117 |
+ |
|
118 |
+ def validate_matchers |
|
119 |
+ matchers = options['matchers'] or return |
|
120 |
+ |
|
121 |
+ unless matchers.is_a?(Array) |
|
122 |
+ errors.add(:base, "matchers must be an array if present") |
|
123 |
+ return |
|
124 |
+ end |
|
125 |
+ |
|
126 |
+ matchers.each do |matcher| |
|
127 |
+ unless matcher.is_a?(Hash) |
|
128 |
+ errors.add(:base, "each matcher must be a hash") |
|
129 |
+ next |
|
130 |
+ end |
|
131 |
+ |
|
132 |
+ regexp, path, to = matcher.values_at(*%w[regexp path to]) |
|
133 |
+ |
|
134 |
+ if regexp.present? |
|
135 |
+ begin |
|
136 |
+ Regexp.new(regexp) |
|
137 |
+ rescue |
|
138 |
+ errors.add(:base, "bad regexp found in matchers: #{regexp}") |
|
139 |
+ end |
|
140 |
+ else |
|
141 |
+ errors.add(:base, "regexp is mandatory for a matcher and must be a string") |
|
142 |
+ end |
|
143 |
+ |
|
144 |
+ errors.add(:base, "path is mandatory for a matcher and must be a string") if !path.present? |
|
145 |
+ |
|
146 |
+ errors.add(:base, "to must be a string if present in a matcher") if to.present? && !to.is_a?(String) |
|
147 |
+ end |
|
148 |
+ end |
|
149 |
+ |
|
150 |
+ def perform_matching(payload) |
|
151 |
+ matchers.inject(payload.dup) { |hash, matcher| |
|
152 |
+ matcher[hash] |
|
153 |
+ } |
|
154 |
+ end |
|
155 |
+ |
|
156 |
+ def matchers |
|
157 |
+ @matchers ||= |
|
158 |
+ if matchers = options['matchers'] |
|
159 |
+ matchers.map { |matcher| |
|
160 |
+ regexp, path, to = matcher.values_at(*%w[regexp path to]) |
|
161 |
+ re = Regexp.new(regexp) |
|
162 |
+ proc { |hash| |
|
163 |
+ mhash = {} |
|
164 |
+ value = Utils.value_at(hash, path) |
|
165 |
+ if value.is_a?(String) && (m = re.match(value)) |
|
166 |
+ m.to_a.each_with_index { |s, i| |
|
167 |
+ mhash[i.to_s] = s |
|
168 |
+ } |
|
169 |
+ m.names.each do |name| |
|
170 |
+ mhash[name] = m[name] |
|
171 |
+ end if m.respond_to?(:names) |
|
172 |
+ end |
|
173 |
+ if to |
|
174 |
+ case value = hash[to] |
|
175 |
+ when Hash |
|
176 |
+ value.update(mhash) |
|
177 |
+ else |
|
178 |
+ hash[to] = mhash |
|
179 |
+ end |
|
180 |
+ else |
|
181 |
+ hash.update(mhash) |
|
182 |
+ end |
|
183 |
+ hash |
|
184 |
+ } |
|
185 |
+ } |
|
186 |
+ else |
|
187 |
+ [] |
|
188 |
+ end |
|
189 |
+ end |
|
190 |
+ |
|
191 |
+ def clear_matchers |
|
192 |
+ @matchers = nil |
|
193 |
+ end |
|
78 | 194 |
end |
79 |
-end |
|
195 |
+end |
@@ -0,0 +1,62 @@ |
||
1 |
+module Agents |
|
2 |
+ class HipchatAgent < Agent |
|
3 |
+ include JsonPathOptionsOverwritable |
|
4 |
+ |
|
5 |
+ cannot_be_scheduled! |
|
6 |
+ cannot_create_events! |
|
7 |
+ |
|
8 |
+ description <<-MD |
|
9 |
+ The HipchatAgent sends messages to a Hipchat Room |
|
10 |
+ |
|
11 |
+ To authenticate you need to set the `auth_token`, you can get one at your Hipchat Group Admin page which you can find here: |
|
12 |
+ |
|
13 |
+ `https://`yoursubdomain`.hipchat.com/admin/api` |
|
14 |
+ |
|
15 |
+ Change the `room_name` to the name of the room you want to send notifications to. |
|
16 |
+ |
|
17 |
+ You can provide a `username` and a `message`. When sending a HTML formatted message change `format` to "html". |
|
18 |
+ If you want your message to notify the room members change `notify` to "true". |
|
19 |
+ Modify the background color of your message via the `color` attribute (one of "yellow", "red", "green", "purple", "gray", or "random") |
|
20 |
+ |
|
21 |
+ If you want to specify either of those attributes per event, you can provide a [JSONPath](http://goessner.net/articles/JsonPath/) for each of them (except the `auth_token`). |
|
22 |
+ MD |
|
23 |
+ |
|
24 |
+ def default_options |
|
25 |
+ { |
|
26 |
+ 'auth_token' => '', |
|
27 |
+ 'room_name' => '', |
|
28 |
+ 'room_name_path' => '', |
|
29 |
+ 'username' => "Huginn", |
|
30 |
+ 'username_path' => '', |
|
31 |
+ 'message' => "Hello from Huginn!", |
|
32 |
+ 'message_path' => '', |
|
33 |
+ 'notify' => false, |
|
34 |
+ 'notify_path' => '', |
|
35 |
+ 'color' => 'yellow', |
|
36 |
+ 'color_path' => '', |
|
37 |
+ } |
|
38 |
+ end |
|
39 |
+ |
|
40 |
+ def validate_options |
|
41 |
+ errors.add(:base, "you need to specify a hipchat auth_token") unless options['auth_token'].present? |
|
42 |
+ errors.add(:base, "you need to specify a room_name or a room_name_path") if options['room_name'].blank? && options['room_name_path'].blank? |
|
43 |
+ end |
|
44 |
+ |
|
45 |
+ def working? |
|
46 |
+ (last_receive_at.present? && last_error_log_at.nil?) || (last_receive_at.present? && last_error_log_at.present? && last_receive_at > last_error_log_at) |
|
47 |
+ end |
|
48 |
+ |
|
49 |
+ def receive(incoming_events) |
|
50 |
+ client = HipChat::Client.new(options[:auth_token]) |
|
51 |
+ incoming_events.each do |event| |
|
52 |
+ mo = merge_json_path_options event |
|
53 |
+ client[mo[:room_name]].send(mo[:username], mo[:message], :notify => mo[:notify].to_s == 'true' ? 1 : 0, :color => mo[:color]) |
|
54 |
+ end |
|
55 |
+ end |
|
56 |
+ |
|
57 |
+ private |
|
58 |
+ def options_with_path |
|
59 |
+ [:room_name, :username, :message, :notify, :color] |
|
60 |
+ end |
|
61 |
+ end |
|
62 |
+end |
@@ -1,10 +1,15 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class PostAgent < Agent |
3 |
- cannot_be_scheduled! |
|
4 | 3 |
cannot_create_events! |
5 | 4 |
|
5 |
+ default_schedule "never" |
|
6 |
+ |
|
6 | 7 |
description <<-MD |
7 |
- Post Agent receives events from other agents and send those events as the contents of a post request to a specified url. `post_url` field must specify where you would like to receive post requests and do not forget to include URI scheme (`http` or `https`) |
|
8 |
+ A PostAgent receives events from other agents (or runs periodically), merges those events with the contents of `payload`, and sends the results as POST (or GET) requests to a specified url. |
|
9 |
+ |
|
10 |
+ The `post_url` field must specify where you would like to send requests. Please include the URI scheme (`http` or `https`). |
|
11 |
+ |
|
12 |
+ The `headers` field is optional. When present, it should be a hash of headers to send with the request. |
|
8 | 13 |
MD |
9 | 14 |
|
10 | 15 |
event_description "Does not produce events." |
@@ -12,7 +17,12 @@ module Agents |
||
12 | 17 |
def default_options |
13 | 18 |
{ |
14 | 19 |
'post_url' => "http://www.example.com", |
15 |
- 'expected_receive_period_in_days' => 1 |
|
20 |
+ 'expected_receive_period_in_days' => 1, |
|
21 |
+ 'method' => 'post', |
|
22 |
+ 'payload' => { |
|
23 |
+ 'key' => 'value' |
|
24 |
+ }, |
|
25 |
+ 'headers' => {} |
|
16 | 26 |
} |
17 | 27 |
end |
18 | 28 |
|
@@ -20,23 +30,71 @@ module Agents |
||
20 | 30 |
last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
21 | 31 |
end |
22 | 32 |
|
33 |
+ def method |
|
34 |
+ (options['method'].presence || 'post').to_s.downcase |
|
35 |
+ end |
|
36 |
+ |
|
37 |
+ def headers |
|
38 |
+ options['headers'].presence || {} |
|
39 |
+ end |
|
40 |
+ |
|
23 | 41 |
def validate_options |
24 | 42 |
unless options['post_url'].present? && options['expected_receive_period_in_days'].present? |
25 | 43 |
errors.add(:base, "post_url and expected_receive_period_in_days are required fields") |
26 | 44 |
end |
27 |
- end |
|
28 | 45 |
|
29 |
- def post_event(uri, event) |
|
30 |
- req = Net::HTTP::Post.new(uri.request_uri) |
|
31 |
- req.form_data = event |
|
32 |
- Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == "https") { |http| http.request(req) } |
|
46 |
+ if options['payload'].present? && !options['payload'].is_a?(Hash) |
|
47 |
+ errors.add(:base, "if provided, payload must be a hash") |
|
48 |
+ end |
|
49 |
+ |
|
50 |
+ unless %w[post get].include?(method) |
|
51 |
+ errors.add(:base, "method must be 'post' or 'get'") |
|
52 |
+ end |
|
53 |
+ |
|
54 |
+ unless headers.is_a?(Hash) |
|
55 |
+ errors.add(:base, "if provided, headers must be a hash") |
|
56 |
+ end |
|
33 | 57 |
end |
34 | 58 |
|
35 | 59 |
def receive(incoming_events) |
36 | 60 |
incoming_events.each do |event| |
37 |
- uri = URI options[:post_url] |
|
38 |
- post_event uri, event.payload |
|
61 |
+ handle (options['payload'].presence || {}).merge(event.payload) |
|
39 | 62 |
end |
40 | 63 |
end |
64 |
+ |
|
65 |
+ def check |
|
66 |
+ handle options['payload'].presence || {} |
|
67 |
+ end |
|
68 |
+ |
|
69 |
+ def generate_uri(params = nil) |
|
70 |
+ uri = URI options[:post_url] |
|
71 |
+ uri.query = URI.encode_www_form(Hash[URI.decode_www_form(uri.query || '')].merge(params)) if params |
|
72 |
+ uri |
|
73 |
+ end |
|
74 |
+ |
|
75 |
+ private |
|
76 |
+ |
|
77 |
+ def handle(data) |
|
78 |
+ if method == 'post' |
|
79 |
+ post_data(data) |
|
80 |
+ elsif method == 'get' |
|
81 |
+ get_data(data) |
|
82 |
+ else |
|
83 |
+ error "Invalid method '#{method}'" |
|
84 |
+ end |
|
85 |
+ end |
|
86 |
+ |
|
87 |
+ def post_data(data) |
|
88 |
+ uri = generate_uri |
|
89 |
+ req = Net::HTTP::Post.new(uri.request_uri, headers) |
|
90 |
+ req.form_data = data |
|
91 |
+ Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == "https") { |http| http.request(req) } |
|
92 |
+ end |
|
93 |
+ |
|
94 |
+ def get_data(data) |
|
95 |
+ uri = generate_uri(data) |
|
96 |
+ req = Net::HTTP::Get.new(uri.request_uri, headers) |
|
97 |
+ Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == "https") { |http| http.request(req) } |
|
98 |
+ end |
|
41 | 99 |
end |
42 | 100 |
end |
@@ -0,0 +1,67 @@ |
||
1 |
+module Agents |
|
2 |
+ class PushbulletAgent < Agent |
|
3 |
+ include JsonPathOptionsOverwritable |
|
4 |
+ |
|
5 |
+ cannot_be_scheduled! |
|
6 |
+ cannot_create_events! |
|
7 |
+ |
|
8 |
+ description <<-MD |
|
9 |
+ The Pushbullet agent sends pushes to a pushbullet device |
|
10 |
+ |
|
11 |
+ To authenticate you need to set the `api_key`, you can find yours at your account page: |
|
12 |
+ |
|
13 |
+ `https://www.pushbullet.com/account` |
|
14 |
+ |
|
15 |
+ Currently you need to get a the device identification manually: |
|
16 |
+ |
|
17 |
+ `curl -u <your api key here>: https://api.pushbullet.com/api/devices` |
|
18 |
+ |
|
19 |
+ Put one of the retured `iden` strings into the `device_id` field. |
|
20 |
+ |
|
21 |
+ You can provide a `title` and a `body`. |
|
22 |
+ |
|
23 |
+ If you want to specify `title` or `body` per event, you can provide a [JSONPath](http://goessner.net/articles/JsonPath/) for each of them. |
|
24 |
+ MD |
|
25 |
+ |
|
26 |
+ def default_options |
|
27 |
+ { |
|
28 |
+ 'api_key' => '', |
|
29 |
+ 'device_id' => '', |
|
30 |
+ 'title' => "Hello from Huginn!", |
|
31 |
+ 'title_path' => '', |
|
32 |
+ 'body' => '', |
|
33 |
+ 'body_path' => '', |
|
34 |
+ } |
|
35 |
+ end |
|
36 |
+ |
|
37 |
+ def validate_options |
|
38 |
+ errors.add(:base, "you need to specify a pushbullet api_key") unless options['api_key'].present? |
|
39 |
+ errors.add(:base, "you need to specify a device_id") if options['device_id'].blank? |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ def working? |
|
43 |
+ received_event_without_error? |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ def receive(incoming_events) |
|
47 |
+ incoming_events.each do |event| |
|
48 |
+ response = HTTParty.post "https://api.pushbullet.com/api/pushes", query_options(event) |
|
49 |
+ error(response.body) if response.body.include? 'error' |
|
50 |
+ end |
|
51 |
+ end |
|
52 |
+ |
|
53 |
+ private |
|
54 |
+ def query_options(event) |
|
55 |
+ mo = merge_json_path_options event |
|
56 |
+ basic_options.deep_merge(:body => {:title => mo[:title], :body => mo[:body]}) |
|
57 |
+ end |
|
58 |
+ |
|
59 |
+ def basic_options |
|
60 |
+ {:basic_auth => {:username =>options[:api_key], :password=>''}, :body => {:device_iden => options[:device_id], :type => 'note'}} |
|
61 |
+ end |
|
62 |
+ |
|
63 |
+ def options_with_path |
|
64 |
+ [:title, :body] |
|
65 |
+ end |
|
66 |
+ end |
|
67 |
+end |
@@ -0,0 +1,107 @@ |
||
1 |
+module Agents |
|
2 |
+ class PushoverAgent < Agent |
|
3 |
+ cannot_be_scheduled! |
|
4 |
+ cannot_create_events! |
|
5 |
+ |
|
6 |
+ API_URL = 'https://api.pushover.net/1/messages.json' |
|
7 |
+ |
|
8 |
+ description <<-MD |
|
9 |
+ The PushoverAgent receives and collects events and sends them via push notification to a user/group. |
|
10 |
+ |
|
11 |
+ **You need a Pushover API Token:** [https://pushover.net/apps/build](https://pushover.net/apps/build) |
|
12 |
+ |
|
13 |
+ **You must provide** a `message` or `text` key that will contain the body of the notification. This can come from an event or be set as a default. Pushover API has a `512` Character Limit including `title`. `message` will be truncated. |
|
14 |
+ |
|
15 |
+ * `token`: your application's API token |
|
16 |
+ * `user`: the user or group key (not e-mail address). |
|
17 |
+ * `expected_receive_period_in_days`: is maximum number of days that you would expect to pass between events being received by this agent. |
|
18 |
+ |
|
19 |
+ Your event can provide any of the following optional parameters or you can provide defaults: |
|
20 |
+ |
|
21 |
+ * `device` - your user's device name to send the message directly to that device, rather than all of the user's devices |
|
22 |
+ * `title` or `subject` - your notifications's title |
|
23 |
+ * `url` - a supplementary URL to show with your message - `512` Character Limit |
|
24 |
+ * `url_title` - a title for your supplementary URL, otherwise just the URL is shown - `100` Character Limit |
|
25 |
+ * `priority` - send as `-1` to always send as a quiet notification, `0` is default, `1` to display as high-priority and bypass the user's quiet hours, or `2` for emergency priority: [Please read Pushover Docs on Emergency Priority](https://pushover.net/api#priority) |
|
26 |
+ * `sound` - the name of one of the sounds supported by device clients to override the user's default sound choice. [See PushOver docs for sound options.](https://pushover.net/api#sounds) |
|
27 |
+ * `retry` - Requred for emergency priority - Specifies how often (in seconds) the Pushover servers will send the same notification to the user. Minimum value: `30` |
|
28 |
+ * `expire` - Requred for emergency priority - Specifies how many seconds your notification will continue to be retried for (every retry seconds). Maximum value: `86400` |
|
29 |
+ |
|
30 |
+ Your event can also pass along a timestamp parameter: |
|
31 |
+ |
|
32 |
+ * `timestamp` - a [Unix timestamp](https://en.wikipedia.org/wiki/Unix_time) of your message's date and time to display to the user, rather than the time your message is received by the Pushover API. |
|
33 |
+ |
|
34 |
+ MD |
|
35 |
+ |
|
36 |
+ def default_options |
|
37 |
+ { |
|
38 |
+ 'token' => '', |
|
39 |
+ 'user' => '', |
|
40 |
+ 'message' => 'a default message', |
|
41 |
+ 'device' => '', |
|
42 |
+ 'title' => '', |
|
43 |
+ 'url' => '', |
|
44 |
+ 'url_title' => '', |
|
45 |
+ 'priority' => 0, |
|
46 |
+ 'sound' => 'pushover', |
|
47 |
+ 'retry' => 0, |
|
48 |
+ 'expire' => 0, |
|
49 |
+ 'expected_receive_period_in_days' => '1' |
|
50 |
+ } |
|
51 |
+ end |
|
52 |
+ |
|
53 |
+ def validate_options |
|
54 |
+ unless options['token'].present? && options['user'].present? && options['expected_receive_period_in_days'].present? |
|
55 |
+ errors.add(:base, 'token, user, and expected_receive_period_in_days are all required.') |
|
56 |
+ end |
|
57 |
+ end |
|
58 |
+ |
|
59 |
+ def receive(incoming_events) |
|
60 |
+ incoming_events.each do |event| |
|
61 |
+ message = (event.payload['message'].presence || event.payload['text'].presence || options['message']).to_s |
|
62 |
+ if message.present? |
|
63 |
+ post_params = { |
|
64 |
+ 'token' => options['token'], |
|
65 |
+ 'user' => options['user'], |
|
66 |
+ 'message' => message |
|
67 |
+ } |
|
68 |
+ |
|
69 |
+ post_params['device'] = event.payload['device'].presence || options['device'] |
|
70 |
+ post_params['title'] = event.payload['title'].presence || event.payload['subject'].presence || options['title'] |
|
71 |
+ |
|
72 |
+ url = (event.payload['url'].presence || options['url'] || '').to_s |
|
73 |
+ url = url.slice 0..512 |
|
74 |
+ post_params['url'] = url |
|
75 |
+ |
|
76 |
+ url_title = (event.payload['url_title'].presence || options['url_title']).to_s |
|
77 |
+ url_title = url_title.slice 0..100 |
|
78 |
+ post_params['url_title'] = url_title |
|
79 |
+ |
|
80 |
+ post_params['priority'] = (event.payload['priority'].presence || options['priority']).to_i |
|
81 |
+ |
|
82 |
+ if event.payload.has_key? 'timestamp' |
|
83 |
+ post_params['timestamp'] = (event.payload['timestamp']).to_s |
|
84 |
+ end |
|
85 |
+ |
|
86 |
+ post_params['sound'] = (event.payload['sound'].presence || options['sound']).to_s |
|
87 |
+ |
|
88 |
+ post_params['retry'] = (event.payload['retry'].presence || options['retry']).to_i |
|
89 |
+ |
|
90 |
+ post_params['expire'] = (event.payload['expire'].presence || options['expire']).to_i |
|
91 |
+ |
|
92 |
+ send_notification(post_params) |
|
93 |
+ end |
|
94 |
+ end |
|
95 |
+ end |
|
96 |
+ |
|
97 |
+ def working? |
|
98 |
+ last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
99 |
+ end |
|
100 |
+ |
|
101 |
+ def send_notification(post_params) |
|
102 |
+ response = HTTParty.post(API_URL, :query => post_params) |
|
103 |
+ puts response |
|
104 |
+ end |
|
105 |
+ |
|
106 |
+ end |
|
107 |
+end |
@@ -0,0 +1,111 @@ |
||
1 |
+require 'open3' |
|
2 |
+ |
|
3 |
+module Agents |
|
4 |
+ class ShellCommandAgent < Agent |
|
5 |
+ default_schedule "never" |
|
6 |
+ |
|
7 |
+ def self.should_run? |
|
8 |
+ ENV['ENABLE_INSECURE_AGENTS'] == "true" |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ description <<-MD |
|
12 |
+ The ShellCommandAgent can execute commands on your local system, returning the output. |
|
13 |
+ |
|
14 |
+ `command` specifies the command to be executed, and `path` will tell ShellCommandAgent in what directory to run this command. |
|
15 |
+ |
|
16 |
+ `expected_update_period_in_days` is used to determine if the Agent is working. |
|
17 |
+ |
|
18 |
+ ShellCommandAgent can also act upon received events. These events may contain their own `path` and `command` values. If they do not, ShellCommandAgent will use the configured options. For this reason, please specify defaults even if you are planning to have this Agent to respond to events. |
|
19 |
+ |
|
20 |
+ The resulting event will contain the `command` which was executed, the `path` it was executed under, the `exit_status` of the command, the `errors`, and the actual `output`. ShellCommandAgent will not log an error if the result implies that something went wrong. |
|
21 |
+ |
|
22 |
+ *Warning*: This type of Agent runs arbitrary commands on your system, #{Agents::ShellCommandAgent.should_run? ? "but is **currently enabled**" : "and is **currently disabled**"}. |
|
23 |
+ Only enable this Agent if you trust everyone using your Huginn installation. |
|
24 |
+ You can enable this Agent in your .env file by setting `ENABLE_INSECURE_AGENTS` to `true`. |
|
25 |
+ MD |
|
26 |
+ |
|
27 |
+ event_description <<-MD |
|
28 |
+ Events look like this: |
|
29 |
+ |
|
30 |
+ { |
|
31 |
+ 'command' => 'pwd', |
|
32 |
+ 'path' => '/home/Huginn', |
|
33 |
+ 'exit_status' => '0', |
|
34 |
+ 'errors' => '', |
|
35 |
+ 'output' => '/home/Huginn' |
|
36 |
+ } |
|
37 |
+ MD |
|
38 |
+ |
|
39 |
+ def default_options |
|
40 |
+ { |
|
41 |
+ 'path' => "/", |
|
42 |
+ 'command' => "pwd", |
|
43 |
+ 'expected_update_period_in_days' => 1 |
|
44 |
+ } |
|
45 |
+ end |
|
46 |
+ |
|
47 |
+ def validate_options |
|
48 |
+ unless options['path'].present? && options['command'].present? && options['expected_update_period_in_days'].present? |
|
49 |
+ errors.add(:base, "The path, command, and expected_update_period_in_days fields are all required.") |
|
50 |
+ end |
|
51 |
+ |
|
52 |
+ unless File.directory?(options['path']) |
|
53 |
+ errors.add(:base, "#{options['path']} is not a real directory.") |
|
54 |
+ end |
|
55 |
+ end |
|
56 |
+ |
|
57 |
+ def working? |
|
58 |
+ Agents::ShellCommandAgent.should_run? && event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
59 |
+ end |
|
60 |
+ |
|
61 |
+ def receive(incoming_events) |
|
62 |
+ incoming_events.each do |event| |
|
63 |
+ handle(event.payload, event) |
|
64 |
+ end |
|
65 |
+ end |
|
66 |
+ |
|
67 |
+ def check |
|
68 |
+ handle(options) |
|
69 |
+ end |
|
70 |
+ |
|
71 |
+ private |
|
72 |
+ |
|
73 |
+ def handle(opts = options, event = nil) |
|
74 |
+ if Agents::ShellCommandAgent.should_run? |
|
75 |
+ command = opts['command'] || options['command'] |
|
76 |
+ path = opts['path'] || options['path'] |
|
77 |
+ |
|
78 |
+ result, errors, exit_status = run_command(path, command) |
|
79 |
+ |
|
80 |
+ vals = {"command" => command, "path" => path, "exit_status" => exit_status, "errors" => errors, "output" => result} |
|
81 |
+ created_event = create_event :payload => vals |
|
82 |
+ |
|
83 |
+ log("Ran '#{command}' under '#{path}'", :outbound_event => created_event, :inbound_event => event) |
|
84 |
+ else |
|
85 |
+ log("Unable to run because insecure agents are not enabled. Edit ENABLE_INSECURE_AGENTS in the Huginn .env configuration.") |
|
86 |
+ end |
|
87 |
+ end |
|
88 |
+ |
|
89 |
+ def run_command(path, command) |
|
90 |
+ result = nil |
|
91 |
+ errors = nil |
|
92 |
+ exit_status = nil |
|
93 |
+ |
|
94 |
+ Dir.chdir(path){ |
|
95 |
+ begin |
|
96 |
+ stdin, stdout, stderr, wait_thr = Open3.popen3(command) |
|
97 |
+ exit_status = wait_thr.value.to_i |
|
98 |
+ result = stdout.gets(nil) |
|
99 |
+ errors = stderr.gets(nil) |
|
100 |
+ rescue Exception => e |
|
101 |
+ errors = e.to_s |
|
102 |
+ end |
|
103 |
+ } |
|
104 |
+ |
|
105 |
+ result = result.to_s.strip |
|
106 |
+ errors = errors.to_s.strip |
|
107 |
+ |
|
108 |
+ [result, errors, exit_status] |
|
109 |
+ end |
|
110 |
+ end |
|
111 |
+end |
@@ -0,0 +1,100 @@ |
||
1 |
+module Agents |
|
2 |
+ class StubhubAgent < Agent |
|
3 |
+ cannot_receive_events! |
|
4 |
+ |
|
5 |
+ description <<-MD |
|
6 |
+ This StubHubAgent creates an event for a given StubHub Event. It can be used to track how many tickets are available for the event and the minimum and maximum price. All that is required is that you paste in the url from the actual event, e.g. http://www.stubhub.com/outside-lands-music-festival-tickets/outside-lands-music-festival-3-day-pass-san-francisco-golden-gate-park-polo-fields-8-8-2014-9020701/ |
|
7 |
+ MD |
|
8 |
+ |
|
9 |
+ event_description <<-MD |
|
10 |
+ Events looks like this: |
|
11 |
+ { |
|
12 |
+ "url": "http://stubhub.com/valid-event-url" |
|
13 |
+ "name": "Event Name" |
|
14 |
+ "date": "2014-08-01" |
|
15 |
+ "max_price": "999.99" |
|
16 |
+ "min_price": "100.99" |
|
17 |
+ "total_postings": "50" |
|
18 |
+ "total_tickets": "150" |
|
19 |
+ "venue_name": "Venue Name" |
|
20 |
+ } |
|
21 |
+ MD |
|
22 |
+ |
|
23 |
+ default_schedule "every_1d" |
|
24 |
+ |
|
25 |
+ def working? |
|
26 |
+ event_created_within?(1) && !recent_error_logs? |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ def default_options |
|
30 |
+ { 'url' => 'http://stubhub.com/enter-your-event-here' } |
|
31 |
+ end |
|
32 |
+ |
|
33 |
+ def validate_options |
|
34 |
+ errors.add(:base, 'url is required') unless options['url'].present? |
|
35 |
+ end |
|
36 |
+ |
|
37 |
+ def url |
|
38 |
+ options['url'] |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ def check |
|
42 |
+ create_event :payload => fetch_stubhub_data(url) |
|
43 |
+ end |
|
44 |
+ |
|
45 |
+ def fetch_stubhub_data(url) |
|
46 |
+ StubhubFetcher.call(url) |
|
47 |
+ end |
|
48 |
+ |
|
49 |
+ class StubhubFetcher |
|
50 |
+ |
|
51 |
+ def self.call(url) |
|
52 |
+ new(url).fields |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ def initialize(url) |
|
56 |
+ @url = url |
|
57 |
+ end |
|
58 |
+ |
|
59 |
+ def event_id |
|
60 |
+ /(\d*)\/{0,1}\z/.match(url)[1] |
|
61 |
+ end |
|
62 |
+ |
|
63 |
+ def base_url |
|
64 |
+ 'http://www.stubhub.com/listingCatalog/select/?q=' |
|
65 |
+ end |
|
66 |
+ |
|
67 |
+ def build_url |
|
68 |
+ base_url + "%2B+stubhubDocumentType%3Aevent%0D%0A%2B+event_id%3A#{event_id}%0D%0A&start=0&rows=10&wt=json" |
|
69 |
+ end |
|
70 |
+ |
|
71 |
+ def response |
|
72 |
+ uri = URI(build_url) |
|
73 |
+ Net::HTTP.get(uri) |
|
74 |
+ end |
|
75 |
+ |
|
76 |
+ def parse_response |
|
77 |
+ JSON.parse(response) |
|
78 |
+ end |
|
79 |
+ |
|
80 |
+ def fields |
|
81 |
+ stubhub_fields = parse_response['response']['docs'][0] |
|
82 |
+ { |
|
83 |
+ 'url' => url, |
|
84 |
+ 'name' => stubhub_fields['seo_description_en_US'], |
|
85 |
+ 'date' => stubhub_fields['event_date_local'], |
|
86 |
+ 'max_price' => stubhub_fields['maxPrice'].to_s, |
|
87 |
+ 'min_price' => stubhub_fields['minPrice'].to_s, |
|
88 |
+ 'total_postings' => stubhub_fields['totalPostings'].to_s, |
|
89 |
+ 'total_tickets' => stubhub_fields['totalTickets'].to_i.to_s, |
|
90 |
+ 'venue_name' => stubhub_fields['venue_name'] |
|
91 |
+ } |
|
92 |
+ end |
|
93 |
+ |
|
94 |
+ private |
|
95 |
+ |
|
96 |
+ attr_reader :url |
|
97 |
+ |
|
98 |
+ end |
|
99 |
+ end |
|
100 |
+end |
@@ -11,8 +11,12 @@ module Agents |
||
11 | 11 |
|
12 | 12 |
The `type` can be one of #{VALID_COMPARISON_TYPES.map { |t| "`#{t}`" }.to_sentence} and compares with the `value`. |
13 | 13 |
|
14 |
+ The `value` can be a single value or an array of values. In the case of an array, if one or more values match then the rule matches. |
|
15 |
+ |
|
14 | 16 |
All rules must match for the Agent to match. The resulting Event will have a payload message of `message`. You can include extractions in the message, for example: `I saw a bar of: <foo.bar>` |
15 | 17 |
|
18 |
+ Set `keep_event` to `true` if you'd like to re-emit the incoming event, optionally merged with 'message' when provided. |
|
19 |
+ |
|
16 | 20 |
Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent. |
17 | 21 |
MD |
18 | 22 |
|
@@ -23,15 +27,20 @@ module Agents |
||
23 | 27 |
MD |
24 | 28 |
|
25 | 29 |
def validate_options |
26 |
- unless options['expected_receive_period_in_days'].present? && options['message'].present? && options['rules'].present? && |
|
30 |
+ unless options['expected_receive_period_in_days'].present? && options['rules'].present? && |
|
27 | 31 |
options['rules'].all? { |rule| rule['type'].present? && VALID_COMPARISON_TYPES.include?(rule['type']) && rule['value'].present? && rule['path'].present? } |
28 | 32 |
errors.add(:base, "expected_receive_period_in_days, message, and rules, with a type, value, and path for every rule, are required") |
29 | 33 |
end |
34 |
+ |
|
35 |
+ errors.add(:base, "message is required unless 'keep_event' is 'true'") unless options['message'].present? || keep_event? |
|
36 |
+ |
|
37 |
+ errors.add(:base, "keep_event, when present, must be 'true' or 'false'") unless options['keep_event'].blank? || %w[true false].include?(options['keep_event']) |
|
30 | 38 |
end |
31 | 39 |
|
32 | 40 |
def default_options |
33 | 41 |
{ |
34 | 42 |
'expected_receive_period_in_days' => "2", |
43 |
+ 'keep_event' => 'false', |
|
35 | 44 |
'rules' => [{ |
36 | 45 |
'type' => "regex", |
37 | 46 |
'value' => "foo\\d+bar", |
@@ -49,33 +58,48 @@ module Agents |
||
49 | 58 |
incoming_events.each do |event| |
50 | 59 |
match = options['rules'].all? do |rule| |
51 | 60 |
value_at_path = Utils.value_at(event['payload'], rule['path']) |
52 |
- case rule['type'] |
|
61 |
+ rule_values = rule['value'] |
|
62 |
+ rule_values = [rule_values] unless rule_values.is_a?(Array) |
|
63 |
+ |
|
64 |
+ match_found = rule_values.any? do |rule_value| |
|
65 |
+ case rule['type'] |
|
53 | 66 |
when "regex" |
54 |
- value_at_path.to_s =~ Regexp.new(rule['value'], Regexp::IGNORECASE) |
|
67 |
+ value_at_path.to_s =~ Regexp.new(rule_value, Regexp::IGNORECASE) |
|
55 | 68 |
when "!regex" |
56 |
- value_at_path.to_s !~ Regexp.new(rule['value'], Regexp::IGNORECASE) |
|
69 |
+ value_at_path.to_s !~ Regexp.new(rule_value, Regexp::IGNORECASE) |
|
57 | 70 |
when "field>value" |
58 |
- value_at_path.to_f > rule['value'].to_f |
|
71 |
+ value_at_path.to_f > rule_value.to_f |
|
59 | 72 |
when "field>=value" |
60 |
- value_at_path.to_f >= rule['value'].to_f |
|
73 |
+ value_at_path.to_f >= rule_value.to_f |
|
61 | 74 |
when "field<value" |
62 |
- value_at_path.to_f < rule['value'].to_f |
|
75 |
+ value_at_path.to_f < rule_value.to_f |
|
63 | 76 |
when "field<=value" |
64 |
- value_at_path.to_f <= rule['value'].to_f |
|
77 |
+ value_at_path.to_f <= rule_value.to_f |
|
65 | 78 |
when "field==value" |
66 |
- value_at_path.to_s == rule['value'].to_s |
|
79 |
+ value_at_path.to_s == rule_value.to_s |
|
67 | 80 |
when "field!=value" |
68 |
- value_at_path.to_s != rule['value'].to_s |
|
81 |
+ value_at_path.to_s != rule_value.to_s |
|
69 | 82 |
else |
70 | 83 |
raise "Invalid type of #{rule['type']} in TriggerAgent##{id}" |
84 |
+ end |
|
71 | 85 |
end |
72 | 86 |
end |
73 | 87 |
|
74 | 88 |
if match |
75 |
- create_event :payload => { 'message' => make_message(event[:payload]) } # Maybe this should include the |
|
76 |
- # original event as well? |
|
89 |
+ if keep_event? |
|
90 |
+ payload = event.payload.dup |
|
91 |
+ payload['message'] = make_message(event[:payload]) if options['message'].present? |
|
92 |
+ else |
|
93 |
+ payload = { 'message' => make_message(event[:payload]) } |
|
94 |
+ end |
|
95 |
+ |
|
96 |
+ create_event :payload => payload |
|
77 | 97 |
end |
78 | 98 |
end |
79 | 99 |
end |
100 |
+ |
|
101 |
+ def keep_event? |
|
102 |
+ options['keep_event'] == 'true' |
|
103 |
+ end |
|
80 | 104 |
end |
81 | 105 |
end |
@@ -7,17 +7,16 @@ module Agents |
||
7 | 7 |
cannot_create_events! |
8 | 8 |
|
9 | 9 |
description <<-MD |
10 |
- The TwilioAgent receives and collects events and sends them via text message or gives you a call when scheduled. |
|
10 |
+ The TwilioAgent receives and collects events and sends them via text message (up to 160 characters) or gives you a call when scheduled. |
|
11 | 11 |
|
12 |
- It is assumed that events have a `message`, `text`, or `sms` key, the value of which is sent as the content of the text message/call. You can use Event Formatting Agent if your event does not provide these keys. |
|
12 |
+ It is assumed that events have a `message`, `text`, or `sms` key, the value of which is sent as the content of the text message/call. You can use the EventFormattingAgent if your event does not provide these keys. |
|
13 | 13 |
|
14 | 14 |
Set `receiver_cell` to the number to receive text messages/call and `sender_cell` to the number sending them. |
15 | 15 |
|
16 | 16 |
`expected_receive_period_in_days` is maximum number of days that you would expect to pass between events being received by this agent. |
17 | 17 |
|
18 |
- If you would like to receive calls, then set `receive_call` to true. `server_url` needs to be |
|
19 |
- filled only if you are making calls. Dont forget to include http/https in `server_url`. |
|
20 |
- |
|
18 |
+ If you would like to receive calls, set `receive_call` to `true`. In this case, `server_url` must be set to the URL of your |
|
19 |
+ Huginn installation (probably "https://#{ENV['DOMAIN']}"), which must be web-accessible. Be sure to set http/https correctly. |
|
21 | 20 |
MD |
22 | 21 |
|
23 | 22 |
def default_options |
@@ -43,13 +42,14 @@ module Agents |
||
43 | 42 |
@client = Twilio::REST::Client.new options['account_sid'], options['auth_token'] |
44 | 43 |
memory['pending_calls'] ||= {} |
45 | 44 |
incoming_events.each do |event| |
46 |
- message = (event.payload['message'] || event.payload['text'] || event.payload['sms']).to_s |
|
47 |
- if message != "" |
|
45 |
+ message = (event.payload['message'].presence || event.payload['text'].presence || event.payload['sms'].presence).to_s |
|
46 |
+ if message.present? |
|
48 | 47 |
if options['receive_call'].to_s == 'true' |
49 | 48 |
secret = SecureRandom.hex 3 |
50 | 49 |
memory['pending_calls'][secret] = message |
51 | 50 |
make_call secret |
52 | 51 |
end |
52 |
+ |
|
53 | 53 |
if options['receive_text'].to_s == 'true' |
54 | 54 |
message = message.slice 0..160 |
55 | 55 |
send_message message |
@@ -71,11 +71,11 @@ module Agents |
||
71 | 71 |
def make_call(secret) |
72 | 72 |
@client.account.calls.create :from => options['sender_cell'], |
73 | 73 |
:to => options['receiver_cell'], |
74 |
- :url => post_url(options['server_url'],secret) |
|
74 |
+ :url => post_url(options['server_url'], secret) |
|
75 | 75 |
end |
76 | 76 |
|
77 |
- def post_url(server_url,secret) |
|
78 |
- "#{server_url}/users/#{self.user.id}/web_requests/#{self.id}/#{secret}" |
|
77 |
+ def post_url(server_url, secret) |
|
78 |
+ "#{server_url}/users/#{user.id}/web_requests/#{id}/#{secret}" |
|
79 | 79 |
end |
80 | 80 |
|
81 | 81 |
def receive_web_request(params, method, format) |
@@ -1,10 +1,10 @@ |
||
1 | 1 |
require 'nokogiri' |
2 |
-require 'typhoeus' |
|
2 |
+require 'faraday' |
|
3 |
+require 'faraday_middleware' |
|
3 | 4 |
require 'date' |
4 | 5 |
|
5 | 6 |
module Agents |
6 | 7 |
class WebsiteAgent < Agent |
7 |
- cannot_receive_events! |
|
8 | 8 |
|
9 | 9 |
default_schedule "every_12h" |
10 | 10 |
|
@@ -16,32 +16,42 @@ module Agents |
||
16 | 16 |
|
17 | 17 |
Specify a `url` and select a `mode` for when to create Events based on the scraped data, either `all` or `on_change`. |
18 | 18 |
|
19 |
+ `url` can be a single url, or an array of urls (for example, for multiple pages with the exact same structure but different content to scrape) |
|
20 |
+ |
|
19 | 21 |
The `type` value can be `xml`, `html`, or `json`. |
20 | 22 |
|
21 | 23 |
To tell the Agent how to parse the content, specify `extract` as a hash with keys naming the extractions and values of hashes. |
22 | 24 |
|
23 |
- When parsing HTML or XML, these sub-hashes specify how to extract with either a `css` CSS selector or a `xpath` XPath expression and either `'text': true` or `attr` pointing to an attribute name to grab. An example: |
|
25 |
+ When parsing HTML or XML, these sub-hashes specify how to extract with either a `css` CSS selector or a `xpath` XPath expression and either `"text": true` or `attr` pointing to an attribute name to grab. An example: |
|
24 | 26 |
|
25 |
- 'extract': { |
|
26 |
- 'url': { 'css': "#comic img", 'attr': "src" }, |
|
27 |
- 'title': { 'css': "#comic img", 'attr': "title" }, |
|
28 |
- 'body_text': { 'css': "div.main", 'text': true } |
|
27 |
+ "extract": { |
|
28 |
+ "url": { "css": "#comic img", "attr": "src" }, |
|
29 |
+ "title": { "css": "#comic img", "attr": "title" }, |
|
30 |
+ "body_text": { "css": "div.main", "text": true } |
|
29 | 31 |
} |
30 | 32 |
|
31 | 33 |
When parsing JSON, these sub-hashes specify [JSONPaths](http://goessner.net/articles/JsonPath/) to the values that you care about. For example: |
32 | 34 |
|
33 |
- 'extract': { |
|
34 |
- 'title': { 'path': "results.data[*].title" }, |
|
35 |
- 'description': { 'path': "results.data[*].description" } |
|
35 |
+ "extract": { |
|
36 |
+ "title": { "path": "results.data[*].title" }, |
|
37 |
+ "description": { "path": "results.data[*].description" } |
|
36 | 38 |
} |
37 | 39 |
|
38 | 40 |
Note that for all of the formats, whatever you extract MUST have the same number of matches for each extractor. E.g., if you're extracting rows, all extractors must match all rows. For generating CSS selectors, something like [SelectorGadget](http://selectorgadget.com) may be helpful. |
39 | 41 |
|
40 |
- Can be configured to use HTTP basic auth by including the `basic_auth` parameter with `username:password`. |
|
42 |
+ Can be configured to use HTTP basic auth by including the `basic_auth` parameter with `"username:password"`, or `["username", "password"]`. |
|
41 | 43 |
|
42 | 44 |
Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent. This is only used to set the "working" status. |
43 | 45 |
|
44 | 46 |
Set `uniqueness_look_back` to limit the number of events checked for uniqueness (typically for performance). This defaults to the larger of #{UNIQUENESS_LOOK_BACK} or #{UNIQUENESS_FACTOR}x the number of detected received results. |
47 |
+ |
|
48 |
+ Set `force_encoding` to an encoding name if the website does not return a Content-Type header with a proper charset. |
|
49 |
+ |
|
50 |
+ Set `user_agent` to a custom User-Agent name if the website does not like the default value ("Faraday v#{Faraday::VERSION}"). |
|
51 |
+ |
|
52 |
+ The `headers` field is optional. When present, it should be a hash of headers to send with the request. |
|
53 |
+ |
|
54 |
+ The WebsiteAgent can also scrape based on incoming events. It will scrape the url contained in the `url` key of the incoming event payload. |
|
45 | 55 |
MD |
46 | 56 |
|
47 | 57 |
event_description do |
@@ -85,88 +95,126 @@ module Agents |
||
85 | 95 |
if options['uniqueness_look_back'].present? |
86 | 96 |
errors.add(:base, "Invalid uniqueness_look_back format") unless is_positive_integer?(options['uniqueness_look_back']) |
87 | 97 |
end |
98 |
+ |
|
99 |
+ if (encoding = options['force_encoding']).present? |
|
100 |
+ case encoding |
|
101 |
+ when String |
|
102 |
+ begin |
|
103 |
+ Encoding.find(encoding) |
|
104 |
+ rescue ArgumentError |
|
105 |
+ errors.add(:base, "Unknown encoding: #{encoding.inspect}") |
|
106 |
+ end |
|
107 |
+ else |
|
108 |
+ errors.add(:base, "force_encoding must be a string") |
|
109 |
+ end |
|
110 |
+ end |
|
111 |
+ |
|
112 |
+ if options['user_agent'].present? |
|
113 |
+ errors.add(:base, "user_agent must be a string") unless options['user_agent'].is_a?(String) |
|
114 |
+ end |
|
115 |
+ |
|
116 |
+ unless headers.is_a?(Hash) |
|
117 |
+ errors.add(:base, "if provided, headers must be a hash") |
|
118 |
+ end |
|
119 |
+ |
|
120 |
+ begin |
|
121 |
+ basic_auth_credentials() |
|
122 |
+ rescue => e |
|
123 |
+ errors.add(:base, e.message) |
|
124 |
+ end |
|
88 | 125 |
end |
89 | 126 |
|
90 | 127 |
def check |
91 |
- hydra = Typhoeus::Hydra.new |
|
92 |
- log "Fetching #{options['url']}" |
|
93 |
- request_opts = { :followlocation => true } |
|
94 |
- request_opts[:userpwd] = options['basic_auth'] if options['basic_auth'].present? |
|
95 |
- request = Typhoeus::Request.new(options['url'], request_opts) |
|
96 |
- |
|
97 |
- request.on_failure do |response| |
|
98 |
- error "Failed: #{response.inspect}" |
|
99 |
- end |
|
128 |
+ check_url options['url'] |
|
129 |
+ end |
|
100 | 130 |
|
101 |
- request.on_success do |response| |
|
102 |
- doc = parse(response.body) |
|
131 |
+ def check_url(in_url) |
|
132 |
+ return unless in_url.present? |
|
103 | 133 |
|
104 |
- if extract_full_json? |
|
105 |
- if store_payload!(previous_payloads(1), doc) |
|
106 |
- log "Storing new result for '#{name}': #{doc.inspect}" |
|
107 |
- create_event :payload => doc |
|
134 |
+ Array(in_url).each do |url| |
|
135 |
+ log "Fetching #{url}" |
|
136 |
+ response = faraday.get(url) |
|
137 |
+ if response.success? |
|
138 |
+ body = response.body |
|
139 |
+ if (encoding = options['force_encoding']).present? |
|
140 |
+ body = body.encode(Encoding::UTF_8, encoding) |
|
108 | 141 |
end |
109 |
- else |
|
110 |
- output = {} |
|
111 |
- options['extract'].each do |name, extraction_details| |
|
112 |
- if extraction_type == "json" |
|
113 |
- result = Utils.values_at(doc, extraction_details['path']) |
|
114 |
- log "Extracting #{extraction_type} at #{extraction_details['path']}: #{result}" |
|
115 |
- else |
|
116 |
- case |
|
117 |
- when css = extraction_details['css'] |
|
118 |
- nodes = doc.css(css) |
|
119 |
- when xpath = extraction_details['xpath'] |
|
120 |
- nodes = doc.xpath(xpath) |
|
142 |
+ doc = parse(body) |
|
143 |
+ |
|
144 |
+ if extract_full_json? |
|
145 |
+ if store_payload!(previous_payloads(1), doc) |
|
146 |
+ log "Storing new result for '#{name}': #{doc.inspect}" |
|
147 |
+ create_event :payload => doc |
|
148 |
+ end |
|
149 |
+ else |
|
150 |
+ output = {} |
|
151 |
+ options['extract'].each do |name, extraction_details| |
|
152 |
+ if extraction_type == "json" |
|
153 |
+ result = Utils.values_at(doc, extraction_details['path']) |
|
154 |
+ log "Extracting #{extraction_type} at #{extraction_details['path']}: #{result}" |
|
121 | 155 |
else |
122 |
- error "'css' or 'xpath' is required for HTML or XML extraction" |
|
123 |
- return |
|
124 |
- end |
|
125 |
- unless Nokogiri::XML::NodeSet === nodes |
|
126 |
- error "The result of HTML/XML extraction was not a NodeSet" |
|
127 |
- return |
|
128 |
- end |
|
129 |
- result = nodes.map { |node| |
|
130 |
- if extraction_details['attr'] |
|
131 |
- node.attr(extraction_details['attr']) |
|
132 |
- elsif extraction_details['text'] |
|
133 |
- node.text() |
|
156 |
+ case |
|
157 |
+ when css = extraction_details['css'] |
|
158 |
+ nodes = doc.css(css) |
|
159 |
+ when xpath = extraction_details['xpath'] |
|
160 |
+ nodes = doc.xpath(xpath) |
|
134 | 161 |
else |
135 |
- error "'attr' or 'text' is required on HTML or XML extraction patterns" |
|
162 |
+ error '"css" or "xpath" is required for HTML or XML extraction' |
|
136 | 163 |
return |
137 | 164 |
end |
138 |
- } |
|
139 |
- log "Extracting #{extraction_type} at #{xpath || css}: #{result}" |
|
165 |
+ unless Nokogiri::XML::NodeSet === nodes |
|
166 |
+ error "The result of HTML/XML extraction was not a NodeSet" |
|
167 |
+ return |
|
168 |
+ end |
|
169 |
+ result = nodes.map { |node| |
|
170 |
+ if extraction_details['attr'] |
|
171 |
+ node.attr(extraction_details['attr']) |
|
172 |
+ elsif extraction_details['text'] |
|
173 |
+ node.text() |
|
174 |
+ else |
|
175 |
+ error '"attr" or "text" is required on HTML or XML extraction patterns' |
|
176 |
+ return |
|
177 |
+ end |
|
178 |
+ } |
|
179 |
+ log "Extracting #{extraction_type} at #{xpath || css}: #{result}" |
|
180 |
+ end |
|
181 |
+ output[name] = result |
|
140 | 182 |
end |
141 |
- output[name] = result |
|
142 |
- end |
|
143 | 183 |
|
144 |
- num_unique_lengths = options['extract'].keys.map { |name| output[name].length }.uniq |
|
184 |
+ num_unique_lengths = options['extract'].keys.map { |name| output[name].length }.uniq |
|
145 | 185 |
|
146 |
- if num_unique_lengths.length != 1 |
|
147 |
- error "Got an uneven number of matches for #{options['name']}: #{options['extract'].inspect}" |
|
148 |
- return |
|
149 |
- end |
|
150 |
- |
|
151 |
- old_events = previous_payloads num_unique_lengths.first |
|
152 |
- num_unique_lengths.first.times do |index| |
|
153 |
- result = {} |
|
154 |
- options['extract'].keys.each do |name| |
|
155 |
- result[name] = output[name][index] |
|
156 |
- if name.to_s == 'url' |
|
157 |
- result[name] = URI.join(options['url'], result[name]).to_s if (result[name] =~ URI::DEFAULT_PARSER.regexp[:ABS_URI]).nil? |
|
158 |
- end |
|
186 |
+ if num_unique_lengths.length != 1 |
|
187 |
+ error "Got an uneven number of matches for #{options['name']}: #{options['extract'].inspect}" |
|
188 |
+ return |
|
159 | 189 |
end |
160 | 190 |
|
161 |
- if store_payload!(old_events, result) |
|
162 |
- log "Storing new parsed result for '#{name}': #{result.inspect}" |
|
163 |
- create_event :payload => result |
|
191 |
+ old_events = previous_payloads num_unique_lengths.first |
|
192 |
+ num_unique_lengths.first.times do |index| |
|
193 |
+ result = {} |
|
194 |
+ options['extract'].keys.each do |name| |
|
195 |
+ result[name] = output[name][index] |
|
196 |
+ if name.to_s == 'url' |
|
197 |
+ result[name] = (response.env[:url] + result[name]).to_s |
|
198 |
+ end |
|
199 |
+ end |
|
200 |
+ |
|
201 |
+ if store_payload!(old_events, result) |
|
202 |
+ log "Storing new parsed result for '#{name}': #{result.inspect}" |
|
203 |
+ create_event :payload => result |
|
204 |
+ end |
|
164 | 205 |
end |
165 | 206 |
end |
207 |
+ else |
|
208 |
+ error "Failed: #{response.inspect}" |
|
166 | 209 |
end |
167 | 210 |
end |
168 |
- hydra.queue request |
|
169 |
- hydra.run |
|
211 |
+ end |
|
212 |
+ |
|
213 |
+ def receive(incoming_events) |
|
214 |
+ incoming_events.each do |event| |
|
215 |
+ url_to_scrape = event.payload['url'] |
|
216 |
+ check_url(url_to_scrape) if url_to_scrape =~ /^https?:\/\//i |
|
217 |
+ end |
|
170 | 218 |
end |
171 | 219 |
|
172 | 220 |
private |
@@ -242,5 +290,47 @@ module Agents |
||
242 | 290 |
false |
243 | 291 |
end |
244 | 292 |
end |
293 |
+ |
|
294 |
+ def faraday |
|
295 |
+ @faraday ||= Faraday.new { |builder| |
|
296 |
+ builder.headers = headers if headers.length > 0 |
|
297 |
+ |
|
298 |
+ if (user_agent = options['user_agent']).present? |
|
299 |
+ builder.headers[:user_agent] = user_agent |
|
300 |
+ end |
|
301 |
+ |
|
302 |
+ builder.use FaradayMiddleware::FollowRedirects |
|
303 |
+ builder.request :url_encoded |
|
304 |
+ if userinfo = basic_auth_credentials() |
|
305 |
+ builder.request :basic_auth, *userinfo |
|
306 |
+ end |
|
307 |
+ |
|
308 |
+ case backend = faraday_backend |
|
309 |
+ when :typhoeus |
|
310 |
+ require 'typhoeus/adapters/faraday' |
|
311 |
+ end |
|
312 |
+ builder.adapter backend |
|
313 |
+ } |
|
314 |
+ end |
|
315 |
+ |
|
316 |
+ def faraday_backend |
|
317 |
+ ENV.fetch('FARADAY_HTTP_BACKEND', 'typhoeus').to_sym |
|
318 |
+ end |
|
319 |
+ |
|
320 |
+ def basic_auth_credentials |
|
321 |
+ case value = options['basic_auth'] |
|
322 |
+ when nil, '' |
|
323 |
+ return nil |
|
324 |
+ when Array |
|
325 |
+ return value if value.size == 2 |
|
326 |
+ when /:/ |
|
327 |
+ return value.split(/:/, 2) |
|
328 |
+ end |
|
329 |
+ raise "bad value for basic_auth: #{value.inspect}" |
|
330 |
+ end |
|
331 |
+ |
|
332 |
+ def headers |
|
333 |
+ options['headers'].presence || {} |
|
334 |
+ end |
|
245 | 335 |
end |
246 | 336 |
end |
@@ -23,8 +23,8 @@ class User < ActiveRecord::Base |
||
23 | 23 |
validates_inclusion_of :invitation_code, :on => :create, :in => INVITATION_CODES, :message => "is not valid" |
24 | 24 |
|
25 | 25 |
has_many :user_credentials, :dependent => :destroy, :inverse_of => :user |
26 |
- has_many :events, :order => "events.created_at desc", :dependent => :delete_all, :inverse_of => :user |
|
27 |
- has_many :agents, :order => "agents.created_at desc", :dependent => :destroy, :inverse_of => :user |
|
26 |
+ has_many :events, -> { order("events.created_at desc") }, :dependent => :delete_all, :inverse_of => :user |
|
27 |
+ has_many :agents, -> { order("agents.created_at desc") }, :dependent => :destroy, :inverse_of => :user |
|
28 | 28 |
has_many :logs, :through => :agents, :class_name => "AgentLog" |
29 | 29 |
|
30 | 30 |
# Allow users to login via either email or username. |
@@ -9,18 +9,7 @@ |
||
9 | 9 |
</div> |
10 | 10 |
|
11 | 11 |
<div class='digraph'> |
12 |
- <% |
|
13 |
- dot_format_string = "digraph foo {" |
|
14 |
- @agents.each.with_index do |agent, index| |
|
15 |
- dot_format_string += "\"#{agent.name}\";" |
|
16 |
- agent.receivers.each do |receiver| |
|
17 |
- dot_format_string += "\"#{agent.name}\"->\"#{receiver.name}\";" |
|
18 |
- end |
|
19 |
- end |
|
20 |
- dot_format_string = dot_format_string + "}" |
|
21 |
- %> |
|
22 |
- |
|
23 |
- <img src="https://chart.googleapis.com/chart?cht=gv&chl=<%= CGI::escape dot_format_string %>" /> |
|
12 |
+ <%= render_agents_diagram(@agents) %> |
|
24 | 13 |
</div> |
25 | 14 |
</div> |
26 | 15 |
</div> |
@@ -18,6 +18,7 @@ |
||
18 | 18 |
<% end %> |
19 | 19 |
<li><%= link_to '<i class="icon-chevron-left"></i> Back'.html_safe, agents_path %></li> |
20 | 20 |
<li><%= link_to '<i class="icon-pencil"></i> Edit'.html_safe, edit_agent_path(@agent) %></li> |
21 |
+ <li><%= link_to '<i class="icon-plus"></i> Clone'.html_safe, new_agent_path(id: @agent) %></li> |
|
21 | 22 |
|
22 | 23 |
<% if @agent.can_be_scheduled? || @agent.events.count > 0 %> |
23 | 24 |
<li class="dropdown"> |
@@ -1,7 +1,7 @@ |
||
1 | 1 |
<% if flash.keys.length > 0 %> |
2 | 2 |
<div class="flash"> |
3 | 3 |
<% flash.each do |name, msg| %> |
4 |
- <div class="alert alert-<%= name == :notice ? "success" : "error" %>"> |
|
4 |
+ <div class="alert alert-<%= name.to_sym == :notice ? "success" : "error" %>"> |
|
5 | 5 |
<a class="close" data-dismiss="alert">×</a> |
6 | 6 |
<%= content_tag :div, msg, :id => "flash_#{name}" if msg.is_a?(String) %> |
7 | 7 |
</div> |
@@ -0,0 +1,3 @@ |
||
1 |
+#!/usr/bin/env ruby |
|
2 |
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) |
|
3 |
+load Gem.bin_path('bundler', 'bundle') |
@@ -0,0 +1,4 @@ |
||
1 |
+#!/usr/bin/env ruby |
|
2 |
+APP_PATH = File.expand_path('../../config/application', __FILE__) |
|
3 |
+require_relative '../config/boot' |
|
4 |
+require 'rails/commands' |
@@ -0,0 +1,4 @@ |
||
1 |
+#!/usr/bin/env ruby |
|
2 |
+require_relative '../config/boot' |
|
3 |
+require 'rake' |
|
4 |
+Rake.application.run |
@@ -64,7 +64,7 @@ class HuginnScheduler |
||
64 | 64 |
|
65 | 65 |
# Schedule repeating events. |
66 | 66 |
|
67 |
- %w[2m 5m 10m 30m 1h 2h 5h 12h 1d 2d 7d].each do |schedule| |
|
67 |
+ %w[1m 2m 5m 10m 30m 1h 2h 5h 12h 1d 2d 7d].each do |schedule| |
|
68 | 68 |
rufus_scheduler.every schedule do |
69 | 69 |
run_schedule "every_#{schedule}" |
70 | 70 |
end |
@@ -2,12 +2,7 @@ require File.expand_path('../boot', __FILE__) |
||
2 | 2 |
|
3 | 3 |
require 'rails/all' |
4 | 4 |
|
5 |
-if defined?(Bundler) |
|
6 |
- # If you precompile assets before deploying to production, use this line |
|
7 |
- Bundler.require(*Rails.groups(:assets => %w(development test))) |
|
8 |
- # If you want your assets lazily compiled in production, use this line |
|
9 |
- # Bundler.require(:default, :assets, Rails.env) |
|
10 |
-end |
|
5 |
+Bundler.require(:default, Rails.env) |
|
11 | 6 |
|
12 | 7 |
module Huginn |
13 | 8 |
class Application < Rails::Application |
@@ -18,10 +13,6 @@ module Huginn |
||
18 | 13 |
# Custom directories with classes and modules you want to be autoloadable. |
19 | 14 |
config.autoload_paths += %W(#{config.root}/lib) |
20 | 15 |
|
21 |
- # Only load the plugins named here, in the order given (default is alphabetical). |
|
22 |
- # :all can be used as a placeholder for all plugins not explicitly named. |
|
23 |
- # config.plugins = [ :exception_notification, :ssl_requirement, :all ] |
|
24 |
- |
|
25 | 16 |
# Activate observers that should always be running. |
26 | 17 |
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer |
27 | 18 |
|
@@ -56,8 +47,5 @@ module Huginn |
||
56 | 47 |
# Enable the asset pipeline |
57 | 48 |
config.assets.enabled = true |
58 | 49 |
config.assets.initialize_on_precompile = false |
59 |
- |
|
60 |
- # Version of your assets, change this if you want to expire all your assets |
|
61 |
- config.assets.version = '1.0' |
|
62 | 50 |
end |
63 | 51 |
end |
@@ -8,8 +8,11 @@ Huginn::Application.configure do |
||
8 | 8 |
# since you don't have to restart the web server when you make code changes. |
9 | 9 |
config.cache_classes = false |
10 | 10 |
|
11 |
- # Log error messages when you accidentally call methods on nil. |
|
12 |
- config.whiny_nils = true |
|
11 |
+ # Eager load code on boot. This eager loads most of Rails and |
|
12 |
+ # your application in memory, allowing both threaded web servers |
|
13 |
+ # and those relying on copy on write to perform better. |
|
14 |
+ # Rake tasks automatically ignore this option for performance. |
|
15 |
+ config.eager_load = false |
|
13 | 16 |
|
14 | 17 |
# Show full error reports and disable caching |
15 | 18 |
config.consider_all_requests_local = true |
@@ -24,12 +27,8 @@ Huginn::Application.configure do |
||
24 | 27 |
# Raise exception on mass assignment protection for Active Record models |
25 | 28 |
config.active_record.mass_assignment_sanitizer = :strict |
26 | 29 |
|
27 |
- # Log the query plan for queries taking more than this (works |
|
28 |
- # with SQLite, MySQL, and PostgreSQL) |
|
29 |
- config.active_record.auto_explain_threshold_in_seconds = 0.5 |
|
30 |
- |
|
31 |
- # Do not compress assets |
|
32 |
- config.assets.compress = false |
|
30 |
+ # Raise an error on page load if there are pending migrations. |
|
31 |
+ config.active_record.migration_error = :page_load |
|
33 | 32 |
|
34 | 33 |
# Expands the lines which load the assets |
35 | 34 |
config.assets.debug = true |
@@ -4,31 +4,41 @@ Huginn::Application.configure do |
||
4 | 4 |
# Code is not reloaded between requests |
5 | 5 |
config.cache_classes = true |
6 | 6 |
|
7 |
+ # Eager load code on boot. This eager loads most of Rails and |
|
8 |
+ # your application in memory, allowing both threaded web servers |
|
9 |
+ # and those relying on copy on write to perform better. |
|
10 |
+ # Rake tasks automatically ignore this option for performance. |
|
11 |
+ config.eager_load = true |
|
12 |
+ |
|
7 | 13 |
# Full error reports are disabled and caching is turned on |
8 | 14 |
config.consider_all_requests_local = false |
9 | 15 |
config.action_controller.perform_caching = true |
10 | 16 |
|
17 |
+ # Enable Rack::Cache to put a simple HTTP cache in front of your application |
|
18 |
+ # Add `rack-cache` to your Gemfile before enabling this. |
|
19 |
+ # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. |
|
20 |
+ # config.action_dispatch.rack_cache = true |
|
21 |
+ |
|
11 | 22 |
# Disable Rails's static asset server (Apache or nginx will already do this) |
12 | 23 |
config.serve_static_assets = false |
13 | 24 |
|
14 | 25 |
# Compress JavaScripts and CSS |
15 |
- config.assets.compress = true |
|
26 |
+ config.assets.js_compressor = :uglifier |
|
27 |
+ config.assets.css_compressor = :sass |
|
16 | 28 |
|
17 | 29 |
# Don't fallback to assets pipeline if a precompiled asset is missed |
18 | 30 |
config.assets.compile = false |
19 | 31 |
|
20 | 32 |
# Generate digests for assets URLs |
21 | 33 |
config.assets.digest = true |
34 |
+ config.assets.precompile += %w(*.png *.jpg *.jpeg *.gif) |
|
22 | 35 |
|
23 |
- # Defaults to nil and saved in location specified by config.assets.prefix |
|
24 |
- # config.assets.manifest = YOUR_PATH |
|
25 |
- |
|
26 |
- # Specifies the header that your server uses for sending files |
|
36 |
+ # Specifies the header that your server uses for sending files. |
|
27 | 37 |
# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache |
28 | 38 |
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx |
29 | 39 |
|
30 | 40 |
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. |
31 |
- config.force_ssl = true |
|
41 |
+ config.force_ssl = ENV['FORCE_SSL'].present? && ENV['FORCE_SSL'] == 'true' ? true : false |
|
32 | 42 |
|
33 | 43 |
# See everything in the log (default is :info) |
34 | 44 |
# config.log_level = :debug |
@@ -50,19 +60,25 @@ Huginn::Application.configure do |
||
50 | 60 |
# Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added) |
51 | 61 |
config.assets.precompile += %w( graphing.js user_credentials.js ) |
52 | 62 |
|
53 |
- # Enable threaded mode |
|
54 |
- # config.threadsafe! |
|
63 |
+ # Ignore bad email addresses and do not raise email delivery errors. |
|
64 |
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors. |
|
65 |
+ # config.action_mailer.raise_delivery_errors = false |
|
55 | 66 |
|
56 | 67 |
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to |
57 |
- # the I18n.default_locale when a translation can not be found) |
|
68 |
+ # the I18n.default_locale when a translation cannot be found). |
|
58 | 69 |
config.i18n.fallbacks = true |
59 | 70 |
|
60 | 71 |
# Send deprecation notices to registered listeners |
61 | 72 |
config.active_support.deprecation = :notify |
62 | 73 |
|
63 |
- # Log the query plan for queries taking more than this (works |
|
64 |
- # with SQLite, MySQL, and PostgreSQL) |
|
65 |
- # config.active_record.auto_explain_threshold_in_seconds = 0.5 |
|
74 |
+ # Disable automatic flushing of the log to improve performance. |
|
75 |
+ # config.autoflush_log = false |
|
76 |
+ |
|
77 |
+ # Use default logging formatter so that PID and timestamp are not suppressed. |
|
78 |
+ config.log_formatter = ::Logger::Formatter.new |
|
79 |
+ |
|
80 |
+ # Do not dump schema after migrations. |
|
81 |
+ config.active_record.dump_schema_after_migration = false |
|
66 | 82 |
|
67 | 83 |
config.action_mailer.default_url_options = { :host => ENV['DOMAIN'] } |
68 | 84 |
config.action_mailer.asset_host = ENV['DOMAIN'] |
@@ -73,4 +89,4 @@ Huginn::Application.configure do |
||
73 | 89 |
config.action_mailer.raise_delivery_errors = true |
74 | 90 |
config.action_mailer.delivery_method = :smtp |
75 | 91 |
# smtp_settings moved to config/initializers/action_mailer.rb |
76 |
-end |
|
92 |
+end |
@@ -1,76 +0,0 @@ |
||
1 |
-Huginn::Application.configure do |
|
2 |
- # Settings specified here will take precedence over those in config/application.rb |
|
3 |
- |
|
4 |
- # Code is not reloaded between requests |
|
5 |
- config.cache_classes = true |
|
6 |
- |
|
7 |
- # Full error reports are disabled and caching is turned on |
|
8 |
- config.consider_all_requests_local = false |
|
9 |
- config.action_controller.perform_caching = true |
|
10 |
- |
|
11 |
- # Disable Rails's static asset server (Apache or nginx will already do this) |
|
12 |
- config.serve_static_assets = false |
|
13 |
- |
|
14 |
- # Compress JavaScripts and CSS |
|
15 |
- config.assets.compress = true |
|
16 |
- |
|
17 |
- # Don't fallback to assets pipeline if a precompiled asset is missed |
|
18 |
- config.assets.compile = false |
|
19 |
- |
|
20 |
- # Generate digests for assets URLs |
|
21 |
- config.assets.digest = true |
|
22 |
- |
|
23 |
- # Defaults to nil and saved in location specified by config.assets.prefix |
|
24 |
- # config.assets.manifest = YOUR_PATH |
|
25 |
- |
|
26 |
- # Specifies the header that your server uses for sending files |
|
27 |
- # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache |
|
28 |
- # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx |
|
29 |
- |
|
30 |
- # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. |
|
31 |
- config.force_ssl = true |
|
32 |
- |
|
33 |
- # See everything in the log (default is :info) |
|
34 |
- # config.log_level = :debug |
|
35 |
- |
|
36 |
- # Prepend all log lines with the following tags |
|
37 |
- config.log_tags = [ :uuid ] # :subdomain |
|
38 |
- |
|
39 |
- # Use a different logger for distributed setups |
|
40 |
- # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) |
|
41 |
- |
|
42 |
- # Use a different cache store in production |
|
43 |
- # config.cache_store = :mem_cache_store |
|
44 |
- |
|
45 |
- # Enable serving of images, stylesheets, and JavaScripts from an asset server |
|
46 |
- if ENV['ASSET_HOST'].present? |
|
47 |
- config.action_controller.asset_host = ENV['ASSET_HOST'] |
|
48 |
- end |
|
49 |
- |
|
50 |
- # Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added) |
|
51 |
- config.assets.precompile += %w( graphing.js ) |
|
52 |
- |
|
53 |
- # Enable threaded mode |
|
54 |
- # config.threadsafe! |
|
55 |
- |
|
56 |
- # Enable locale fallbacks for I18n (makes lookups for any locale fall back to |
|
57 |
- # the I18n.default_locale when a translation can not be found) |
|
58 |
- config.i18n.fallbacks = true |
|
59 |
- |
|
60 |
- # Send deprecation notices to registered listeners |
|
61 |
- config.active_support.deprecation = :notify |
|
62 |
- |
|
63 |
- # Log the query plan for queries taking more than this (works |
|
64 |
- # with SQLite, MySQL, and PostgreSQL) |
|
65 |
- # config.active_record.auto_explain_threshold_in_seconds = 0.5 |
|
66 |
- |
|
67 |
- config.action_mailer.default_url_options = { :host => ENV['DOMAIN'] } |
|
68 |
- config.action_mailer.asset_host = ENV['DOMAIN'] |
|
69 |
- if ENV['ASSET_HOST'] |
|
70 |
- config.action_mailer.asset_host = ENV['ASSET_HOST'] |
|
71 |
- end |
|
72 |
- config.action_mailer.perform_deliveries = true |
|
73 |
- config.action_mailer.raise_delivery_errors = true |
|
74 |
- config.action_mailer.delivery_method = :smtp |
|
75 |
- # smtp_settings moved to config/initializers/action_mailer.rb |
|
76 |
-end |
@@ -7,13 +7,15 @@ Huginn::Application.configure do |
||
7 | 7 |
# and recreated between test runs. Don't rely on the data there! |
8 | 8 |
config.cache_classes = true |
9 | 9 |
|
10 |
+ # Do not eager load code on boot. This avoids loading your whole application |
|
11 |
+ # just for the purpose of running a single test. If you are using a tool that |
|
12 |
+ # preloads Rails for running tests, you may have to set it to true. |
|
13 |
+ config.eager_load = false |
|
14 |
+ |
|
10 | 15 |
# Configure static asset server for tests with Cache-Control for performance |
11 | 16 |
config.serve_static_assets = true |
12 | 17 |
config.static_cache_control = "public, max-age=3600" |
13 | 18 |
|
14 |
- # Log error messages when you accidentally call methods on nil |
|
15 |
- config.whiny_nils = true |
|
16 |
- |
|
17 | 19 |
# Show full error reports and disable caching |
18 | 20 |
config.consider_all_requests_local = true |
19 | 21 |
config.action_controller.perform_caching = false |
@@ -22,7 +24,7 @@ Huginn::Application.configure do |
||
22 | 24 |
config.action_dispatch.show_exceptions = false |
23 | 25 |
|
24 | 26 |
# Disable request forgery protection in test environment |
25 |
- config.action_controller.allow_forgery_protection = false |
|
27 |
+ config.action_controller.allow_forgery_protection = false |
|
26 | 28 |
|
27 | 29 |
# Tell Action Mailer not to deliver emails to the real world. |
28 | 30 |
# The :test delivery method accumulates sent emails in the |
@@ -3,7 +3,8 @@ |
||
3 | 3 |
Devise.setup do |config| |
4 | 4 |
# ==> Mailer Configuration |
5 | 5 |
# Configure the e-mail address which will be shown in Devise::Mailer, |
6 |
- # note that it will be overwritten if you use your own mailer class with default "from" parameter. |
|
6 |
+ # note that it will be overwritten if you use your own mailer class |
|
7 |
+ # with default "from" parameter. |
|
7 | 8 |
config.mailer_sender = "please-change-me-at-config-initializers-devise@example.com" |
8 | 9 |
|
9 | 10 |
# Configure the class responsible to send e-mails. |
@@ -72,6 +73,12 @@ Devise.setup do |config| |
||
72 | 73 |
# passing :skip => :sessions to `devise_for` in your config/routes.rb |
73 | 74 |
config.skip_session_storage = [:http_auth] |
74 | 75 |
|
76 |
+ # By default, Devise cleans up the CSRF token on authentication to |
|
77 |
+ # avoid CSRF token fixation attacks. This means that, when using AJAX |
|
78 |
+ # requests for sign in and sign up, you need to get a new CSRF token |
|
79 |
+ # from the server. You can disable this option at your own risk. |
|
80 |
+ # config.clean_up_csrf_token_on_authentication = true |
|
81 |
+ |
|
75 | 82 |
# ==> Configuration for :database_authenticatable |
76 | 83 |
# For bcrypt, this is the cost for hashing the password and defaults to 10. If |
77 | 84 |
# using other encryptors, it sets how many times you want the password re-encrypted. |
@@ -174,10 +181,6 @@ Devise.setup do |config| |
||
174 | 181 |
# REST_AUTH_SITE_KEY to pepper) |
175 | 182 |
# config.encryptor = :sha512 |
176 | 183 |
|
177 |
- # ==> Configuration for :token_authenticatable |
|
178 |
- # Defines name of the authentication token params key |
|
179 |
- # config.token_authentication_key = :auth_token |
|
180 |
- |
|
181 | 184 |
# ==> Scopes configuration |
182 | 185 |
# Turn scoped views on. Before rendering "sessions/new", it will first check for |
183 | 186 |
# "users/sessions/new". It's turned off by default because it's slower if you |
@@ -4,4 +4,4 @@ |
||
4 | 4 |
# If you change this key, all old signed cookies will become invalid! |
5 | 5 |
# Make sure the secret is at least 30 characters and all random, |
6 | 6 |
# no regular words or you'll be exposed to dictionary attacks. |
7 |
-Huginn::Application.config.secret_token = ENV['APP_SECRET_TOKEN'] |
|
7 |
+Huginn::Application.config.secret_key_base = ENV['APP_SECRET_TOKEN'] |
@@ -28,18 +28,18 @@ Huginn::Application.routes.draw do |
||
28 | 28 |
|
29 | 29 |
resources :user_credentials, :except => :show |
30 | 30 |
|
31 |
- match "/worker_status" => "worker_status#show" |
|
31 |
+ get "/worker_status" => "worker_status#show" |
|
32 | 32 |
|
33 | 33 |
post "/users/:user_id/update_location/:secret" => "user_location_updates#create" |
34 | 34 |
|
35 |
- match "/users/:user_id/web_requests/:agent_id/:secret" => "web_requests#handle_request", :as => :web_requests |
|
35 |
+ match "/users/:user_id/web_requests/:agent_id/:secret" => "web_requests#handle_request", :as => :web_requests, :via => [:get, :post, :put, :delete] |
|
36 | 36 |
post "/users/:user_id/webhooks/:agent_id/:secret" => "web_requests#handle_request" # legacy |
37 | 37 |
|
38 | 38 |
# To enable DelayedJobWeb, see the 'Enable DelayedJobWeb' section of the README. |
39 |
-# match "/delayed_job" => DelayedJobWeb, :anchor => false |
|
39 |
+# get "/delayed_job" => DelayedJobWeb, :anchor => false |
|
40 | 40 |
|
41 | 41 |
devise_for :users, :sign_out_via => [ :post, :delete ] |
42 | 42 |
|
43 |
- match "/about" => "home#about" |
|
43 |
+ get "/about" => "home#about" |
|
44 | 44 |
root :to => "home#index" |
45 | 45 |
end |
@@ -1,7 +1,7 @@ |
||
1 | 1 |
# This file should contain all the record creation needed to seed the database with its default values. |
2 | 2 |
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). |
3 | 3 |
|
4 |
-user = User.find_or_initialize_by_email("admin@example.com") |
|
4 |
+user = User.find_or_initialize_by(:email => "admin@example.com") |
|
5 | 5 |
user.username = "admin" |
6 | 6 |
user.password = "password" |
7 | 7 |
user.password_confirmation = "password" |
@@ -0,0 +1,3 @@ |
||
1 |
+cookbook_path ["cookbooks", "site-cookbooks"] |
|
2 |
+role_path "roles" |
|
3 |
+data_bag_path "data_bags" |
@@ -0,0 +1,71 @@ |
||
1 |
+SITE |
|
2 |
+ remote: http://community.opscode.com/api/v1 |
|
3 |
+ specs: |
|
4 |
+ apt (2.3.8) |
|
5 |
+ bluepill (2.3.1) |
|
6 |
+ rsyslog (>= 0.0.0) |
|
7 |
+ build-essential (2.0.0) |
|
8 |
+ chef_handler (1.1.6) |
|
9 |
+ dmg (2.2.0) |
|
10 |
+ ohai (1.1.12) |
|
11 |
+ rsyslog (1.12.2) |
|
12 |
+ runit (1.5.10) |
|
13 |
+ build-essential (>= 0.0.0) |
|
14 |
+ yum (~> 3.0) |
|
15 |
+ yum-epel (>= 0.0.0) |
|
16 |
+ windows (1.30.2) |
|
17 |
+ chef_handler (>= 0.0.0) |
|
18 |
+ yum (3.2.0) |
|
19 |
+ yum-epel (0.3.6) |
|
20 |
+ yum (~> 3.0) |
|
21 |
+ |
|
22 |
+GIT |
|
23 |
+ remote: git://github.com/mdxp/nodejs-cookbook.git |
|
24 |
+ ref: master |
|
25 |
+ sha: e2415cd8c4e03dccf21d7ef6ca31e1c5c81467ca |
|
26 |
+ specs: |
|
27 |
+ nodejs (1.3.0) |
|
28 |
+ apt (>= 0.0.0) |
|
29 |
+ build-essential (>= 0.0.0) |
|
30 |
+ yum-epel (>= 0.0.0) |
|
31 |
+ |
|
32 |
+GIT |
|
33 |
+ remote: git://github.com/opscode-cookbooks/git.git |
|
34 |
+ ref: master |
|
35 |
+ sha: 76b0f9bb08fdd9e2e201fd70b72298097accdf96 |
|
36 |
+ specs: |
|
37 |
+ git (4.0.1) |
|
38 |
+ build-essential (>= 0.0.0) |
|
39 |
+ dmg (>= 0.0.0) |
|
40 |
+ runit (>= 1.0) |
|
41 |
+ windows (>= 0.0.0) |
|
42 |
+ yum (~> 3.0) |
|
43 |
+ yum-epel (>= 0.0.0) |
|
44 |
+ |
|
45 |
+GIT |
|
46 |
+ remote: git://github.com/opscode-cookbooks/mysql.git |
|
47 |
+ ref: master |
|
48 |
+ sha: a2ff53f0ca6deca75aebf6da55ac381194ec7728 |
|
49 |
+ specs: |
|
50 |
+ mysql (5.1.9) |
|
51 |
+ |
|
52 |
+GIT |
|
53 |
+ remote: git://github.com/opscode-cookbooks/nginx.git |
|
54 |
+ ref: master |
|
55 |
+ sha: 05b3a613f53a0b05c96f9206c5d67aa420f337fb |
|
56 |
+ specs: |
|
57 |
+ nginx (2.6.3) |
|
58 |
+ apt (~> 2.2) |
|
59 |
+ bluepill (~> 2.3) |
|
60 |
+ build-essential (~> 2.0) |
|
61 |
+ ohai (~> 1.1) |
|
62 |
+ runit (~> 1.2) |
|
63 |
+ yum-epel (~> 0.3) |
|
64 |
+ |
|
65 |
+DEPENDENCIES |
|
66 |
+ git (>= 0) |
|
67 |
+ mysql (>= 0) |
|
68 |
+ nginx (>= 0) |
|
69 |
+ nodejs (>= 0) |
|
70 |
+ runit (>= 0) |
|
71 |
+ |
@@ -3,37 +3,38 @@ |
||
3 | 3 |
|
4 | 4 |
Vagrant.configure("2") do |config| |
5 | 5 |
config.omnibus.chef_version = :latest |
6 |
- config.vm.define :vb do |vb| |
|
7 |
- vb.vm.box = "precise32" |
|
8 |
- vb.vm.box_url = "http://files.vagrantup.com/precise32.box" |
|
9 |
- vb.vm.network :forwarded_port, host: 3000, guest: 3000 |
|
10 | 6 |
|
11 |
- vb.vm.provision :chef_solo do |chef| |
|
12 |
- chef.roles_path = "roles" |
|
13 |
- chef.cookbooks_path = ["cookbooks", "site-cookbooks"] |
|
14 |
- chef.add_role("huginn_development") |
|
15 |
- end |
|
7 |
+ config.vm.provision :chef_solo do |chef| |
|
8 |
+ chef.roles_path = "roles" |
|
9 |
+ chef.cookbooks_path = ["cookbooks", "site-cookbooks"] |
|
10 |
+ chef.add_role("huginn_development") |
|
11 |
+ # chef.add_role("huginn_production") |
|
16 | 12 |
end |
17 | 13 |
|
18 |
- config.vm.define :ec2 do |ec2| |
|
19 |
- ec2.vm.box = "dummy" |
|
20 |
- ec2.vm.box_url = "https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box" |
|
14 |
+ config.vm.provider :virtualbox do |vb, override| |
|
15 |
+ #vb.memory = 1024 |
|
16 |
+ #vb.cpus = 4 |
|
17 |
+ override.vm.box = "hashicorp/precise64" |
|
18 |
+ override.vm.network :forwarded_port, host: 3000, guest: 3000 |
|
19 |
+ end |
|
20 |
+ |
|
21 |
+ config.vm.provider :parallels do |prl, override| |
|
22 |
+ override.vm.box = "parallels/ubuntu-12.04" |
|
23 |
+ end |
|
24 |
+ |
|
25 |
+ config.vm.provider :aws do |aws, override| |
|
26 |
+ aws.ami = ENV['AWS_AMI'] || "ami-828675f5" |
|
27 |
+ aws.region = ENV['AWS_REGION'] || "eu-west-1" |
|
28 |
+ aws.instance_type = "t1.micro" |
|
21 | 29 |
|
22 |
- ec2.vm.provider :aws do |aws, override| |
|
23 |
- aws.access_key_id = "" |
|
24 |
- aws.secret_access_key = "" |
|
25 |
- aws.keypair_name = "" |
|
26 |
- aws.region = "us-east-1" |
|
27 |
- aws.ami = "ami-d0f89fb9" |
|
30 |
+ override.vm.box = "dummy" |
|
31 |
+ override.vm.box_url = "https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box" |
|
32 |
+ override.ssh.private_key_path = ENV["AWS_SSH_PRIVKEY"] |
|
33 |
+ override.ssh.username = ENV['AWS_SSH_USER'] || "ubuntu" |
|
28 | 34 |
|
29 |
- override.ssh.username = "ubuntu" |
|
30 |
- override.ssh.private_key_path = "" |
|
31 |
- end |
|
32 |
- ec2.vm.provision :chef_solo do |chef| |
|
33 |
- chef.roles_path = "roles" |
|
34 |
- chef.cookbooks_path = ["cookbooks", "site-cookbooks"] |
|
35 |
- chef.add_role("huginn_production") |
|
36 |
- |
|
37 |
- end |
|
35 |
+ aws.access_key_id = ENV["AWS_ACCESS_KEY_ID"] |
|
36 |
+ aws.secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"] |
|
37 |
+ aws.keypair_name = ENV["AWS_KEYPAIR_NAME"] |
|
38 |
+ aws.security_groups = [ ENV["AWS_SECURITY_GROUP"] ] |
|
38 | 39 |
end |
39 | 40 |
end |
@@ -23,6 +23,7 @@ |
||
23 | 23 |
"recipe[git]", |
24 | 24 |
"recipe[apt]", |
25 | 25 |
"recipe[mysql::server]", |
26 |
+ "recipe[mysql::client]", |
|
26 | 27 |
"recipe[nodejs::install_from_binary]", |
27 | 28 |
"recipe[huginn_development]" |
28 | 29 |
] |
@@ -10,7 +10,7 @@ |
||
10 | 10 |
|
11 | 11 |
"default_attributes" : { |
12 | 12 |
"mysql": { |
13 |
- "server_root_password": "", |
|
13 |
+ "server_root_password": "password", |
|
14 | 14 |
"server_repl_password": "", |
15 | 15 |
"server_debian_password": "" |
16 | 16 |
}, |
@@ -16,12 +16,19 @@ group "huginn" do |
||
16 | 16 |
action :create |
17 | 17 |
end |
18 | 18 |
|
19 |
-%w("ruby1.9.1" "ruby1.9.1-dev" "libxslt-dev" "libxml2-dev" "curl").each do |pkg| |
|
19 |
+%w("ruby1.9.1" "ruby1.9.1-dev" "libxslt-dev" "libxml2-dev" "curl" "libmysqlclient-dev" "rubygems").each do |pkg| |
|
20 | 20 |
package pkg do |
21 | 21 |
action :install |
22 | 22 |
end |
23 | 23 |
end |
24 | 24 |
|
25 |
+bash "Setting default ruby version to 1.9" do |
|
26 |
+ code <<-EOH |
|
27 |
+ update-alternatives --set ruby /usr/bin/ruby1.9.1 |
|
28 |
+ update-alternatives --set gem /usr/bin/gem1.9.1 |
|
29 |
+ EOH |
|
30 |
+end |
|
31 |
+ |
|
25 | 32 |
git "/home/huginn/huginn" do |
26 | 33 |
repository 'git://github.com/cantino/huginn.git' |
27 | 34 |
reference 'master' |
@@ -48,10 +55,10 @@ bash "huginn dependencies" do |
||
48 | 55 |
export LANG="en_US.UTF-8" |
49 | 56 |
export LC_ALL="en_US.UTF-8" |
50 | 57 |
sudo bundle install |
51 |
- sed s/REPLACE_ME_NOW\!/$(sudo rake secret)/ .env.example > .env |
|
52 |
- sudo rake db:create |
|
53 |
- sudo rake db:migrate |
|
54 |
- sudo rake db:seed |
|
58 |
+ sed s/REPLACE_ME_NOW\!/$(sudo bundle exec rake secret)/ .env.example > .env |
|
59 |
+ sudo bundle exec rake db:create |
|
60 |
+ sudo bundle exec rake db:migrate |
|
61 |
+ sudo bundle exec rake db:seed |
|
55 | 62 |
EOH |
56 | 63 |
end |
57 | 64 |
|
@@ -59,6 +66,6 @@ bash "huginn has been installed and will start in a minute" do |
||
59 | 66 |
user "huginn" |
60 | 67 |
cwd "/home/huginn/huginn" |
61 | 68 |
code <<-EOH |
62 |
- sudo foreman start |
|
69 |
+ sudo nohup foreman start & |
|
63 | 70 |
EOH |
64 | 71 |
end |
@@ -1,58 +0,0 @@ |
||
1 |
-source 'https://rubygems.org' |
|
2 |
- |
|
3 |
-gem 'rails' |
|
4 |
-gem 'rake' |
|
5 |
-gem 'mysql2' |
|
6 |
-gem 'devise' |
|
7 |
-gem 'kaminari' |
|
8 |
-gem 'bootstrap-kaminari-views' |
|
9 |
-gem "rufus-scheduler", :require => false |
|
10 |
-gem 'json', '>= 1.7.7' |
|
11 |
-gem 'jsonpath' |
|
12 |
-gem 'twilio-ruby' |
|
13 |
- |
|
14 |
-gem 'delayed_job', :git => 'https://github.com/wok/delayed_job' # Until the YAML issues are fixed in master. |
|
15 |
-gem 'delayed_job_active_record', "~> 0.3.3" # newer was giving a strange MySQL error |
|
16 |
-gem "daemons" |
|
17 |
-# gem "delayed_job_web" |
|
18 |
-group :production do |
|
19 |
- gem 'unicorn' |
|
20 |
-end |
|
21 |
-gem 'foreman' |
|
22 |
-gem 'dotenv-rails', :groups => [:development, :test] |
|
23 |
- |
|
24 |
-group :assets do |
|
25 |
- gem 'sass-rails', '~> 3.2.3' |
|
26 |
- gem 'coffee-rails', '~> 3.2.1' |
|
27 |
- gem 'uglifier', '>= 1.0.3' |
|
28 |
- gem 'select2-rails' |
|
29 |
- gem 'jquery-rails' |
|
30 |
-end |
|
31 |
- |
|
32 |
-gem 'geokit-rails3' |
|
33 |
-gem 'kramdown' |
|
34 |
-gem "typhoeus" |
|
35 |
-gem 'nokogiri' |
|
36 |
-gem 'wunderground' |
|
37 |
- |
|
38 |
-gem "twitter" |
|
39 |
-gem 'twitter-stream', '>=0.1.16' |
|
40 |
-gem 'em-http-request' |
|
41 |
- |
|
42 |
-platforms :ruby_18 do |
|
43 |
- gem 'system_timer' |
|
44 |
- gem 'fastercsv' |
|
45 |
-end |
|
46 |
- |
|
47 |
-group :development do |
|
48 |
- gem 'pry' |
|
49 |
-end |
|
50 |
- |
|
51 |
-group :development, :test do |
|
52 |
- gem 'rspec-rails' |
|
53 |
- gem 'rspec' |
|
54 |
- gem 'shoulda-matchers' |
|
55 |
- gem 'rr' |
|
56 |
- gem 'webmock', :require => false |
|
57 |
- gem 'rake' |
|
58 |
-end |
@@ -1,4 +1,4 @@ |
||
1 |
-web: sudo bundle exec unicorn_rails -c config/unicorn.rb |
|
2 |
-schedule: sudo bundle exec rails runner bin/schedule.rb |
|
3 |
-twitter: sudo bundle exec rails runner bin/twitter_stream.rb |
|
4 |
-dj: sudo bundle exec script/delayed_job run |
|
1 |
+web: sudo bundle exec unicorn_rails -c config/unicorn.rb -E production |
|
2 |
+schedule: sudo RAILS_ENV=production bundle exec rails runner bin/schedule.rb |
|
3 |
+twitter: sudo RAILS_ENV=production bundle exec rails runner bin/twitter_stream.rb |
|
4 |
+dj: sudo RAILS_ENV=production bundle exec script/delayed_job run |
@@ -3,18 +3,22 @@ |
||
3 | 3 |
# Replace the following with the output from "rake secret" |
4 | 4 |
APP_SECRET_TOKEN=REPLACE_ME_NOW! |
5 | 5 |
|
6 |
-# This is the domain where your Huginn instance will be running. The default should work |
|
7 |
-# for development, but it needs to be changed when you deploy to a production environment. |
|
6 |
+# This is the domain where your Huginn instance will be running. The default should work |
|
7 |
+# for development, but it needs to be changed to your Huginn domain when you deploy to a |
|
8 |
+# production environment (e.g., yourdomain.com, possibly including a port). |
|
8 | 9 |
#DOMAIN=localhost:3000 |
9 | 10 |
|
10 |
-# Database Setup |
|
11 |
+############################ |
|
12 |
+# Database Setup # |
|
13 |
+############################ |
|
14 |
+ |
|
11 | 15 |
DATABASE_ADAPTER=mysql2 |
12 | 16 |
DATABASE_ENCODING=utf8 |
13 | 17 |
DATABASE_RECONNECT=true |
14 | 18 |
DATABASE_NAME=huginn_production |
15 | 19 |
DATABASE_POOL=5 |
16 | 20 |
DATABASE_USERNAME=root |
17 |
-DATABASE_PASSWORD= |
|
21 |
+DATABASE_PASSWORD=password |
|
18 | 22 |
#DATABASE_HOST=your-domain-here.com |
19 | 23 |
#DATABASE_PORT=3306 |
20 | 24 |
#DATABASE_SOCKET=/tmp/mysql.sock |
@@ -24,8 +28,27 @@ DATABASE_PASSWORD= |
||
24 | 28 |
# Configure Rails environment. This should only be needed in production and may cause errors in development. |
25 | 29 |
RAILS_ENV=production |
26 | 30 |
|
31 |
+# Should Rails force all requests to use SSL? |
|
32 |
+FORCE_SSL=false |
|
33 |
+ |
|
34 |
+############################ |
|
35 |
+# Allowing Signups # |
|
36 |
+############################ |
|
37 |
+ |
|
38 |
+# This invitation code will be required for users to signup with your Huginn installation. |
|
39 |
+# You can see its use in user.rb. PLEASE CHANGE THIS! |
|
40 |
+INVITATION_CODE=try-huginn |
|
41 |
+ |
|
42 |
+############################# |
|
43 |
+# Email Configuration # |
|
44 |
+############################# |
|
45 |
+ |
|
27 | 46 |
# Outgoing email settings. To use Gmail or Google Apps, put your Google Apps domain or gmail.com |
28 | 47 |
# as the SMTP_DOMAIN and your Gmail username and password as the SMTP_USER_NAME and SMTP_PASSWORD. |
48 |
+# |
|
49 |
+# PLEASE NOTE: In order to enable emails locally (e.g., when not in the production Rails environment), |
|
50 |
+# you must also change config.action_mailer.perform_deliveries in config/environments/development.rb. |
|
51 |
+ |
|
29 | 52 |
SMTP_DOMAIN=your-domain-here.com |
30 | 53 |
SMTP_USER_NAME=you@gmail.com |
31 | 54 |
SMTP_PASSWORD=somepassword |
@@ -37,6 +60,44 @@ SMTP_ENABLE_STARTTLS_AUTO=true |
||
37 | 60 |
# The address from which system emails will appear to be sent. |
38 | 61 |
EMAIL_FROM_ADDRESS=from_address@gmail.com |
39 | 62 |
|
40 |
-# This invitation code will be required for users to signup with your Huginn installation. |
|
41 |
-# You can see its use in user.rb. |
|
42 |
-INVITATION_CODE=try-huginn |
|
63 |
+########################### |
|
64 |
+# Agent Logging # |
|
65 |
+########################### |
|
66 |
+ |
|
67 |
+# Number of lines of log messages to keep per Agent |
|
68 |
+AGENT_LOG_LENGTH=200 |
|
69 |
+ |
|
70 |
+############################# |
|
71 |
+# AWS and Mechanical Turk # |
|
72 |
+############################# |
|
73 |
+ |
|
74 |
+# AWS Credentials for MTurk |
|
75 |
+AWS_ACCESS_KEY_ID="your aws access key id" |
|
76 |
+AWS_ACCESS_KEY="your aws access key" |
|
77 |
+ |
|
78 |
+# Set AWS_SANDBOX to true if you're developing Huginn code. |
|
79 |
+AWS_SANDBOX=false |
|
80 |
+ |
|
81 |
+######################## |
|
82 |
+# Various Settings # |
|
83 |
+######################## |
|
84 |
+ |
|
85 |
+# Specify the HTTP backend library for Faraday, used in WebsiteAgent. |
|
86 |
+# You can change this depending on the performance and stability you |
|
87 |
+# need for your service. Any choice other than "typhoeus", |
|
88 |
+# "net_http", or "em_http" should require you to bundle a corresponding |
|
89 |
+# gem via Gemfile. |
|
90 |
+FARADAY_HTTP_BACKEND=typhoeus |
|
91 |
+ |
|
92 |
+# Allow JSONPath eval expresions. i.e., $..price[?(@ < 20)] |
|
93 |
+# You should not allow this on a shared Huginn box because it is not secure. |
|
94 |
+ALLOW_JSONPATH_EVAL=false |
|
95 |
+ |
|
96 |
+# Enable this setting to allow insecure Agents like the ShellCommandAgent. Only do this |
|
97 |
+# when you trust everyone using your Huginn installation. |
|
98 |
+ENABLE_INSECURE_AGENTS=false |
|
99 |
+ |
|
100 |
+# Use Graphviz for generating diagrams instead of using Google Chart |
|
101 |
+# Tools. Specify a dot(1) command path built with SVG support |
|
102 |
+# enabled. |
|
103 |
+#USE_GRAPHVIZ_DOT=dot |
@@ -1,15 +1,18 @@ |
||
1 | 1 |
#worker_process 2; |
2 | 2 |
user huginn huginn; |
3 | 3 |
|
4 |
-events { |
|
4 |
+events { |
|
5 | 5 |
worker_connections 1024; |
6 | 6 |
accept_mutex on; |
7 | 7 |
} |
8 | 8 |
|
9 | 9 |
http { |
10 |
+ types_hash_max_size 2048; |
|
11 |
+ include mime.types; |
|
12 |
+ |
|
10 | 13 |
upstream huginn_server { |
11 | 14 |
server unix:/home/huginn/shared/tmp/sockets/unicorn.sock; |
12 |
-} |
|
15 |
+ } |
|
13 | 16 |
|
14 | 17 |
server { |
15 | 18 |
listen 80; |
@@ -23,13 +26,9 @@ http { |
||
23 | 26 |
} |
24 | 27 |
location @app { |
25 | 28 |
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
26 |
- |
|
27 | 29 |
proxy_set_header X-Forwarded-Proto $scheme; |
28 |
- |
|
29 | 30 |
proxy_set_header Host $http_host; |
30 |
- |
|
31 | 31 |
proxy_redirect off; |
32 |
- |
|
33 | 32 |
proxy_pass http://huginn_server; |
34 | 33 |
} |
35 | 34 |
} |
@@ -17,7 +17,8 @@ stdout_path "log/unicorn_err.log" |
||
17 | 17 |
pid '/home/huginn/shared/tmp/pids/unicorn.pid' |
18 | 18 |
|
19 | 19 |
before_fork do |server, worker| |
20 |
- ActiveRecord::Base.connection.disconnect! |
|
20 |
+ defined?(ActiveRecord::Base) and |
|
21 |
+ ActiveRecord::Base.connection.disconnect! |
|
21 | 22 |
old_pid = "#{server.config[:pid]}.oldbin" |
22 | 23 |
if File.exists?(old_pid) && server.pid != old_pid |
23 | 24 |
begin |
@@ -29,5 +30,6 @@ before_fork do |server, worker| |
||
29 | 30 |
end |
30 | 31 |
|
31 | 32 |
after_fork do |server, worker| |
32 |
- ActiveRecord::Base.establish_connection |
|
33 |
+ defined?(ActiveRecord::Base) and |
|
34 |
+ ActiveRecord::Base.establish_connection |
|
33 | 35 |
end |
@@ -14,10 +14,17 @@ group "huginn" do |
||
14 | 14 |
members ["huginn"] |
15 | 15 |
end |
16 | 16 |
|
17 |
-%w("ruby1.9.1" "ruby1.9.1-dev" "libxslt-dev" "libxml2-dev" "curl" "libshadow-ruby1.8").each do |pkg| |
|
17 |
+%w("ruby1.9.1" "ruby1.9.1-dev" "libxslt-dev" "libxml2-dev" "curl" "libshadow-ruby1.8" "libmysqlclient-dev" "libffi-dev" "libssl-dev" "rubygems").each do |pkg| |
|
18 | 18 |
package("#{pkg}") |
19 | 19 |
end |
20 | 20 |
|
21 |
+bash "Setting default ruby version to 1.9" do |
|
22 |
+ code <<-EOH |
|
23 |
+ update-alternatives --set ruby /usr/bin/ruby1.9.1 |
|
24 |
+ update-alternatives --set gem /usr/bin/gem1.9.1 |
|
25 |
+ EOH |
|
26 |
+end |
|
27 |
+ |
|
21 | 28 |
gem_package("rake") |
22 | 29 |
gem_package("bundle") |
23 | 30 |
|
@@ -36,6 +43,7 @@ end |
||
36 | 43 |
|
37 | 44 |
deploy "/home/huginn" do |
38 | 45 |
repo "https://github.com/cantino/huginn.git" |
46 |
+ branch "master" |
|
39 | 47 |
user "huginn" |
40 | 48 |
group "huginn" |
41 | 49 |
environment "RAILS_ENV" => "production" |
@@ -56,7 +64,7 @@ deploy "/home/huginn" do |
||
56 | 64 |
end |
57 | 65 |
directory("/home/huginn/shared/tmp/pids") |
58 | 66 |
directory("/home/huginn/shared/tmp/sockets") |
59 |
- %w(Procfile unicorn.rb Gemfile nginx.conf).each do |file| |
|
67 |
+ %w(Procfile unicorn.rb nginx.conf).each do |file| |
|
60 | 68 |
cookbook_file "/home/huginn/shared/config/#{file}" do |
61 | 69 |
owner "huginn" |
62 | 70 |
action :create_if_missing |
@@ -77,16 +85,17 @@ deploy "/home/huginn" do |
||
77 | 85 |
code <<-EOH |
78 | 86 |
export LANG="en_US.UTF-8" |
79 | 87 |
export LC_ALL="en_US.UTF-8" |
80 |
- ln -nfs /home/huginn/shared/config/Gemfile ./Gemfile |
|
81 | 88 |
ln -nfs /home/huginn/shared/config/Procfile ./Procfile |
82 | 89 |
ln -nfs /home/huginn/shared/config/.env ./.env |
83 | 90 |
ln -nfs /home/huginn/shared/config/unicorn.rb ./config/unicorn.rb |
84 |
- sudo cp /home/huginn/shared/config/nginx.conf /etc/nginx/ |
|
85 |
- sudo bundle install |
|
86 |
- sed -i s/REPLACE_ME_NOW\!/$(sudo rake secret)/ .env |
|
87 |
- sudo rake db:create |
|
88 |
- sudo rake db:migrate |
|
89 |
- sudo rake db:seed |
|
91 |
+ sudo cp /home/huginn/shared/config/nginx.conf /etc/nginx/ |
|
92 |
+ echo 'gem "unicorn", :group => :production' >> Gemfile |
|
93 |
+ sudo bundle install --without=development --without=test |
|
94 |
+ sed -i s/REPLACE_ME_NOW\!/$(sudo bundle exec rake secret)/ /home/huginn/shared/config/.env |
|
95 |
+ sudo RAILS_ENV=production bundle exec rake db:create |
|
96 |
+ sudo RAILS_ENV=production bundle exec rake db:migrate |
|
97 |
+ sudo RAILS_ENV=production bundle exec rake db:seed |
|
98 |
+ sudo RAILS_ENV=production bundle exec rake assets:precompile |
|
90 | 99 |
sudo foreman export upstart /etc/init -a huginn -u huginn -l log |
91 | 100 |
sudo start huginn |
92 | 101 |
EOH |
@@ -1,6 +0,0 @@ |
||
1 |
-file_cache_path "/tmp/chef-solo" |
|
2 |
-data_bag_path "/tmp/chef-solo/data_bags" |
|
3 |
-encrypted_data_bag_secret "/tmp/chef-solo/data_bag_key" |
|
4 |
-cookbook_path [ "/tmp/chef-solo/site-cookbooks", |
|
5 |
- "/tmp/chef-solo/cookbooks" ] |
|
6 |
-role_path "/tmp/chef-solo/roles" |
@@ -1,10 +1,10 @@ |
||
1 | 1 |
module RDBMSFunctions |
2 | 2 |
def rdbms_date_add(source, unit, amount) |
3 |
- adapter_type = connection.adapter_name.downcase.to_sym |
|
3 |
+ adapter_type = ActiveRecord::Base.connection.adapter_name.downcase.to_sym |
|
4 | 4 |
case adapter_type |
5 | 5 |
when :mysql, :mysql2 |
6 | 6 |
"DATE_ADD(`#{source}`, INTERVAL #{amount} #{unit})" |
7 |
- when :postgresql |
|
7 |
+ when :postgresql |
|
8 | 8 |
"(#{source} + INTERVAL '#{amount} #{unit}')" |
9 | 9 |
else |
10 | 10 |
raise NotImplementedError, "Unknown adapter type '#{adapter_type}'" |
@@ -56,7 +56,7 @@ module Utils |
||
56 | 56 |
escape = false |
57 | 57 |
end |
58 | 58 |
|
59 |
- result = JsonPath.new(path, :allow_eval => false).on(data.is_a?(String) ? data : data.to_json) |
|
59 |
+ result = JsonPath.new(path, :allow_eval => ENV['ALLOW_JSONPATH_EVAL'] == "true").on(data.is_a?(String) ? data : data.to_json) |
|
60 | 60 |
if escape |
61 | 61 |
result.map {|r| CGI::escape r } |
62 | 62 |
else |
@@ -79,4 +79,4 @@ module Utils |
||
79 | 79 |
def self.pretty_jsonify(thing) |
80 | 80 |
JSON.pretty_generate(thing).gsub('</', '<\/') |
81 | 81 |
end |
82 |
-end |
|
82 |
+end |
@@ -46,6 +46,22 @@ describe AgentsController do |
||
46 | 46 |
end |
47 | 47 |
end |
48 | 48 |
|
49 |
+ describe "GET new with :id" do |
|
50 |
+ it "opens a clone of a given Agent" do |
|
51 |
+ sign_in users(:bob) |
|
52 |
+ get :new, :id => agents(:bob_website_agent).to_param |
|
53 |
+ assigns(:agent).attributes.should eq(users(:bob).agents.build_clone(agents(:bob_website_agent)).attributes) |
|
54 |
+ end |
|
55 |
+ |
|
56 |
+ it "only allows the current user to clone his own Agent" do |
|
57 |
+ sign_in users(:bob) |
|
58 |
+ |
|
59 |
+ lambda { |
|
60 |
+ get :new, :id => agents(:jane_website_agent).to_param |
|
61 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
62 |
+ end |
|
63 |
+ end |
|
64 |
+ |
|
49 | 65 |
describe "GET edit" do |
50 | 66 |
it "only shows Agents for the current user" do |
51 | 67 |
sign_in users(:bob) |
@@ -0,0 +1,21 @@ |
||
1 |
+[ |
|
2 |
+ { |
|
3 |
+ "creator": { |
|
4 |
+ "fullsize_avatar_url": "https://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/original.gif?r=3", |
|
5 |
+ "avatar_url": "http://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/avatar.gif?r=3", |
|
6 |
+ "name": "Dominik Sander", |
|
7 |
+ "id": 123456 |
|
8 |
+ }, |
|
9 |
+ "attachments": [], |
|
10 |
+ "raw_excerpt": "test test", |
|
11 |
+ "excerpt": "test test", |
|
12 |
+ "id": 6454342343, |
|
13 |
+ "created_at": "2014-04-17T10:25:31.000+02:00", |
|
14 |
+ "updated_at": "2014-04-17T10:25:31.000+02:00", |
|
15 |
+ "summary": "commented on whaat", |
|
16 |
+ "action": "commented on", |
|
17 |
+ "target": "whaat", |
|
18 |
+ "url": "https://basecamp.com/12456/api/v1/projects/5476464-explore-basecamp/messages/24598238-whaat.json", |
|
19 |
+ "html_url": "https://basecamp.com/12456/projects/5476464-explore-basecamp/messages/24598238-whaat#comment_150756301" |
|
20 |
+ } |
|
21 |
+] |
@@ -0,0 +1,17 @@ |
||
1 |
+{ |
|
2 |
+ "response":{ |
|
3 |
+ "docs":[ |
|
4 |
+ { |
|
5 |
+ "url": "http://www.stubhub.com/event/name-1-1-2014-12345", |
|
6 |
+ "seo_description_en_US": "name", |
|
7 |
+ "event_date_local": "2014-01-01", |
|
8 |
+ "maxPrice": "100", |
|
9 |
+ "minPrice": "50", |
|
10 |
+ "totalPostings": "100", |
|
11 |
+ "totalTickets": "200", |
|
12 |
+ "venue_name": "Venue Name" |
|
13 |
+ } |
|
14 |
+ ] |
|
15 |
+ } |
|
16 |
+} |
|
17 |
+ |
@@ -0,0 +1,48 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe DotHelper do |
|
4 |
+ describe "#dot_id" do |
|
5 |
+ it "properly escapes double quotaion and backslash" do |
|
6 |
+ dot_id('hello\\"').should == '"hello\\\\\\""' |
|
7 |
+ end |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ describe "with example Agents" do |
|
11 |
+ class Agents::DotFoo < Agent |
|
12 |
+ default_schedule "2pm" |
|
13 |
+ |
|
14 |
+ def check |
|
15 |
+ create_event :payload => {} |
|
16 |
+ end |
|
17 |
+ end |
|
18 |
+ |
|
19 |
+ class Agents::DotBar < Agent |
|
20 |
+ cannot_be_scheduled! |
|
21 |
+ |
|
22 |
+ def check |
|
23 |
+ create_event :payload => {} |
|
24 |
+ end |
|
25 |
+ end |
|
26 |
+ |
|
27 |
+ before do |
|
28 |
+ stub(Agents::DotFoo).valid_type?("Agents::DotFoo") { true } |
|
29 |
+ stub(Agents::DotBar).valid_type?("Agents::DotBar") { true } |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ describe "#agents_dot" do |
|
33 |
+ it "generates a DOT script" do |
|
34 |
+ @foo = Agents::DotFoo.new(:name => "foo") |
|
35 |
+ @foo.user = users(:bob) |
|
36 |
+ @foo.save! |
|
37 |
+ |
|
38 |
+ @bar = Agents::DotBar.new(:name => "bar") |
|
39 |
+ @bar.user = users(:bob) |
|
40 |
+ @bar.sources << @foo |
|
41 |
+ @bar.save! |
|
42 |
+ |
|
43 |
+ agents_dot([@foo, @bar]).should == 'digraph foo {"foo";"foo"->"bar";"bar";}' |
|
44 |
+ agents_dot([@foo, @bar], true).should == 'digraph foo {"foo"[URL="/agents/%d"];"foo"->"bar";"bar"[URL="/agents/%d"];}' % [@foo.id, @bar.id] |
|
45 |
+ end |
|
46 |
+ end |
|
47 |
+ end |
|
48 |
+end |
@@ -97,7 +97,7 @@ describe Utils do |
||
97 | 97 |
it "escapes </script> tags in the output JSON" do |
98 | 98 |
cleaned_json = Utils.jsonify(:foo => "bar", :xss => "</script><script>alert('oh no!')</script>") |
99 | 99 |
cleaned_json.should_not include("</script>") |
100 |
- cleaned_json.should include("<\\/script>") |
|
100 |
+ cleaned_json.should include('\\u003c/script\\u003e') |
|
101 | 101 |
end |
102 | 102 |
|
103 | 103 |
it "html_safes the output unless :skip_safe is passed in" do |
@@ -1,6 +1,9 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
+require 'models/concerns/working_helpers' |
|
2 | 3 |
|
3 | 4 |
describe Agent do |
5 |
+ it_behaves_like WorkingHelpers |
|
6 |
+ |
|
4 | 7 |
describe ".run_schedule" do |
5 | 8 |
before do |
6 | 9 |
Agents::WeatherAgent.count.should > 0 |
@@ -514,6 +517,51 @@ describe Agent do |
||
514 | 517 |
end |
515 | 518 |
end |
516 | 519 |
end |
520 |
+ |
|
521 |
+ describe "Agent.build_clone" do |
|
522 |
+ before do |
|
523 |
+ Event.delete_all |
|
524 |
+ @sender = Agents::SomethingSource.new( |
|
525 |
+ name: 'Agent (2)', |
|
526 |
+ options: { foo: 'bar2' }, |
|
527 |
+ schedule: '5pm') |
|
528 |
+ @sender.user = users(:bob) |
|
529 |
+ @sender.save! |
|
530 |
+ @sender.create_event :payload => {} |
|
531 |
+ @sender.create_event :payload => {} |
|
532 |
+ @sender.events.count.should == 2 |
|
533 |
+ |
|
534 |
+ @receiver = Agents::CannotBeScheduled.new( |
|
535 |
+ name: 'Agent', |
|
536 |
+ options: { foo: 'bar3' }, |
|
537 |
+ keep_events_for: 3, |
|
538 |
+ propagate_immediately: true) |
|
539 |
+ @receiver.user = users(:bob) |
|
540 |
+ @receiver.sources << @sender |
|
541 |
+ @receiver.memory[:test] = 1 |
|
542 |
+ @receiver.save! |
|
543 |
+ end |
|
544 |
+ |
|
545 |
+ it "should create a clone of a given agent for editing" do |
|
546 |
+ sender_clone = users(:bob).agents.build_clone(@sender) |
|
547 |
+ |
|
548 |
+ sender_clone.attributes.should == Agent.new.attributes. |
|
549 |
+ update(@sender.slice(:user_id, :type, |
|
550 |
+ :options, :schedule, :keep_events_for, :propagate_immediately)). |
|
551 |
+ update('name' => 'Agent (2) (2)', 'options' => { 'foo' => 'bar2' }) |
|
552 |
+ |
|
553 |
+ sender_clone.source_ids.should == [] |
|
554 |
+ |
|
555 |
+ receiver_clone = users(:bob).agents.build_clone(@receiver) |
|
556 |
+ |
|
557 |
+ receiver_clone.attributes.should == Agent.new.attributes. |
|
558 |
+ update(@receiver.slice(:user_id, :type, |
|
559 |
+ :options, :schedule, :keep_events_for, :propagate_immediately)). |
|
560 |
+ update('name' => 'Agent (3)', 'options' => { 'foo' => 'bar3' }) |
|
561 |
+ |
|
562 |
+ receiver_clone.source_ids.should == [@sender.id] |
|
563 |
+ end |
|
564 |
+ end |
|
517 | 565 |
end |
518 | 566 |
|
519 | 567 |
describe ".trigger_web_request" do |
@@ -565,32 +613,6 @@ describe Agent do |
||
565 | 613 |
end |
566 | 614 |
end |
567 | 615 |
|
568 |
- describe "recent_error_logs?" do |
|
569 |
- it "returns true if last_error_log_at is near last_event_at" do |
|
570 |
- agent = Agent.new |
|
571 |
- |
|
572 |
- agent.last_error_log_at = 10.minutes.ago |
|
573 |
- agent.last_event_at = 10.minutes.ago |
|
574 |
- agent.recent_error_logs?.should be_true |
|
575 |
- |
|
576 |
- agent.last_error_log_at = 11.minutes.ago |
|
577 |
- agent.last_event_at = 10.minutes.ago |
|
578 |
- agent.recent_error_logs?.should be_true |
|
579 |
- |
|
580 |
- agent.last_error_log_at = 5.minutes.ago |
|
581 |
- agent.last_event_at = 10.minutes.ago |
|
582 |
- agent.recent_error_logs?.should be_true |
|
583 |
- |
|
584 |
- agent.last_error_log_at = 15.minutes.ago |
|
585 |
- agent.last_event_at = 10.minutes.ago |
|
586 |
- agent.recent_error_logs?.should be_false |
|
587 |
- |
|
588 |
- agent.last_error_log_at = 2.days.ago |
|
589 |
- agent.last_event_at = 10.minutes.ago |
|
590 |
- agent.recent_error_logs?.should be_false |
|
591 |
- end |
|
592 |
- end |
|
593 |
- |
|
594 | 616 |
describe "scopes" do |
595 | 617 |
describe "of_type" do |
596 | 618 |
it "should accept classes" do |
@@ -0,0 +1,85 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Agents::BasecampAgent do |
|
4 |
+ before(:each) do |
|
5 |
+ stub_request(:get, /json$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"}) |
|
6 |
+ stub_request(:get, /Z$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"}) |
|
7 |
+ @valid_params = { |
|
8 |
+ :username => "user", |
|
9 |
+ :password => "pass", |
|
10 |
+ :user_id => 12345, |
|
11 |
+ :project_id => 6789, |
|
12 |
+ } |
|
13 |
+ |
|
14 |
+ @checker = Agents::BasecampAgent.new(:name => "somename", :options => @valid_params) |
|
15 |
+ @checker.user = users(:jane) |
|
16 |
+ @checker.save! |
|
17 |
+ end |
|
18 |
+ |
|
19 |
+ describe "validating" do |
|
20 |
+ before do |
|
21 |
+ @checker.should be_valid |
|
22 |
+ end |
|
23 |
+ |
|
24 |
+ it "should require the basecamp username" do |
|
25 |
+ @checker.options['username'] = nil |
|
26 |
+ @checker.should_not be_valid |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ it "should require the basecamp password" do |
|
30 |
+ @checker.options['password'] = nil |
|
31 |
+ @checker.should_not be_valid |
|
32 |
+ end |
|
33 |
+ |
|
34 |
+ it "should require the basecamp user_id" do |
|
35 |
+ @checker.options['user_id'] = nil |
|
36 |
+ @checker.should_not be_valid |
|
37 |
+ end |
|
38 |
+ |
|
39 |
+ it "should require the basecamp project_id" do |
|
40 |
+ @checker.options['project_id'] = nil |
|
41 |
+ @checker.should_not be_valid |
|
42 |
+ end |
|
43 |
+ |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ describe "helpers" do |
|
47 |
+ it "should generate a correct request options hash" do |
|
48 |
+ @checker.send(:request_options).should == {:basic_auth=>{:username=>"user", :password=>"pass"}, :headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)"}} |
|
49 |
+ end |
|
50 |
+ |
|
51 |
+ it "should generate the currect request url" do |
|
52 |
+ @checker.send(:request_url).should == "https://basecamp.com/12345/api/v1/projects/6789/events.json" |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ |
|
56 |
+ it "should not provide the since attribute on first run" do |
|
57 |
+ @checker.send(:query_parameters).should == {} |
|
58 |
+ end |
|
59 |
+ |
|
60 |
+ it "should provide the since attribute after the first run" do |
|
61 |
+ time = (Time.now-1.minute).iso8601 |
|
62 |
+ @checker.memory[:last_run] = time |
|
63 |
+ @checker.save |
|
64 |
+ @checker.reload.send(:query_parameters).should == {:query => {:since => time}} |
|
65 |
+ end |
|
66 |
+ end |
|
67 |
+ describe "#check" do |
|
68 |
+ it "should not emit events on its first run" do |
|
69 |
+ expect { @checker.check }.to change { Event.count }.by(0) |
|
70 |
+ end |
|
71 |
+ it "should check that initial run creates an event" do |
|
72 |
+ @checker.last_check_at = Time.now - 1.minute |
|
73 |
+ expect { @checker.check }.to change { Event.count }.by(1) |
|
74 |
+ end |
|
75 |
+ end |
|
76 |
+ |
|
77 |
+ describe "#working?" do |
|
78 |
+ it "it is working when at least one event was emited" do |
|
79 |
+ @checker.should_not be_working |
|
80 |
+ @checker.last_check_at = Time.now - 1.minute |
|
81 |
+ @checker.check |
|
82 |
+ @checker.reload.should be_working |
|
83 |
+ end |
|
84 |
+ end |
|
85 |
+end |
@@ -7,9 +7,16 @@ describe Agents::EventFormattingAgent do |
||
7 | 7 |
:options => { |
8 | 8 |
:instructions => { |
9 | 9 |
:message => "Received <$.content.text.*> from <$.content.name> .", |
10 |
- :subject => "Weather looks like <$.conditions>" |
|
10 |
+ :subject => "Weather looks like <$.conditions> according to the forecast at <$.pretty_date.time>" |
|
11 | 11 |
}, |
12 | 12 |
:mode => "clean", |
13 |
+ :matchers => [ |
|
14 |
+ { |
|
15 |
+ :path => "$.date.pretty", |
|
16 |
+ :regexp => "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)", |
|
17 |
+ :to => "pretty_date", |
|
18 |
+ }, |
|
19 |
+ ], |
|
13 | 20 |
:skip_agent => "false", |
14 | 21 |
:skip_created_at => "false" |
15 | 22 |
} |
@@ -24,7 +31,11 @@ describe Agents::EventFormattingAgent do |
||
24 | 31 |
@event.payload = { |
25 | 32 |
:content => { |
26 | 33 |
:text => "Some Lorem Ipsum", |
27 |
- :name => "somevalue" |
|
34 |
+ :name => "somevalue", |
|
35 |
+ }, |
|
36 |
+ :date => { |
|
37 |
+ :epoch => "1357959600", |
|
38 |
+ :pretty => "10:00 PM EST on January 11, 2013" |
|
28 | 39 |
}, |
29 | 40 |
:conditions => "someothervalue" |
30 | 41 |
} |
@@ -61,7 +72,11 @@ describe Agents::EventFormattingAgent do |
||
61 | 72 |
it "should handle JSONPaths in instructions" do |
62 | 73 |
@checker.receive([@event]) |
63 | 74 |
Event.last.payload[:message].should == "Received Some Lorem Ipsum from somevalue ." |
64 |
- Event.last.payload[:subject].should == "Weather looks like someothervalue" |
|
75 |
+ end |
|
76 |
+ |
|
77 |
+ it "should handle matchers and JSONPaths in instructions" do |
|
78 |
+ @checker.receive([@event]) |
|
79 |
+ Event.last.payload[:subject].should == "Weather looks like someothervalue according to the forecast at 10:00 PM EST" |
|
65 | 80 |
end |
66 | 81 |
|
67 | 82 |
it "should allow escaping" do |
@@ -110,6 +125,28 @@ describe Agents::EventFormattingAgent do |
||
110 | 125 |
@checker.should_not be_valid |
111 | 126 |
end |
112 | 127 |
|
128 |
+ it "should validate type of matchers" do |
|
129 |
+ @checker.options[:matchers] = "" |
|
130 |
+ @checker.should_not be_valid |
|
131 |
+ @checker.options[:matchers] = {} |
|
132 |
+ @checker.should_not be_valid |
|
133 |
+ end |
|
134 |
+ |
|
135 |
+ it "should validate the contents of matchers" do |
|
136 |
+ @checker.options[:matchers] = [ |
|
137 |
+ {} |
|
138 |
+ ] |
|
139 |
+ @checker.should_not be_valid |
|
140 |
+ @checker.options[:matchers] = [ |
|
141 |
+ { :regexp => "(not closed", :path => "text" } |
|
142 |
+ ] |
|
143 |
+ @checker.should_not be_valid |
|
144 |
+ @checker.options[:matchers] = [ |
|
145 |
+ { :regexp => "(closed)", :path => "text", :to => "foo" } |
|
146 |
+ ] |
|
147 |
+ @checker.should be_valid |
|
148 |
+ end |
|
149 |
+ |
|
113 | 150 |
it "should validate presence of mode" do |
114 | 151 |
@checker.options[:mode] = "" |
115 | 152 |
@checker.should_not be_valid |
@@ -125,4 +162,4 @@ describe Agents::EventFormattingAgent do |
||
125 | 162 |
@checker.should_not be_valid |
126 | 163 |
end |
127 | 164 |
end |
128 |
-end |
|
165 |
+end |
@@ -0,0 +1,83 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+require 'models/concerns/json_path_options_overwritable' |
|
3 |
+ |
|
4 |
+describe Agents::HipchatAgent do |
|
5 |
+ it_behaves_like JsonPathOptionsOverwritable |
|
6 |
+ |
|
7 |
+ before(:each) do |
|
8 |
+ @valid_params = { |
|
9 |
+ 'auth_token' => 'token', |
|
10 |
+ 'room_name' => 'test', |
|
11 |
+ 'room_name_path' => '', |
|
12 |
+ 'username' => "Huginn", |
|
13 |
+ 'username_path' => '$.username', |
|
14 |
+ 'message' => "Hello from Huginn!", |
|
15 |
+ 'message_path' => '$.message', |
|
16 |
+ 'notify' => false, |
|
17 |
+ 'notify_path' => '', |
|
18 |
+ 'color' => 'yellow', |
|
19 |
+ 'color_path' => '', |
|
20 |
+ } |
|
21 |
+ |
|
22 |
+ @checker = Agents::HipchatAgent.new(:name => "somename", :options => @valid_params) |
|
23 |
+ @checker.user = users(:jane) |
|
24 |
+ @checker.save! |
|
25 |
+ |
|
26 |
+ @event = Event.new |
|
27 |
+ @event.agent = agents(:bob_weather_agent) |
|
28 |
+ @event.payload = { :room_name => 'test room', :message => 'Looks like its going to rain', username: "Huggin user"} |
|
29 |
+ @event.save! |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ describe "validating" do |
|
33 |
+ before do |
|
34 |
+ @checker.should be_valid |
|
35 |
+ end |
|
36 |
+ |
|
37 |
+ it "should require the basecamp username" do |
|
38 |
+ @checker.options['auth_token'] = nil |
|
39 |
+ @checker.should_not be_valid |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ it "should require the basecamp password" do |
|
43 |
+ @checker.options['room_name'] = nil |
|
44 |
+ @checker.should_not be_valid |
|
45 |
+ end |
|
46 |
+ |
|
47 |
+ it "should require the basecamp user_id" do |
|
48 |
+ @checker.options['room_name'] = nil |
|
49 |
+ @checker.options['room_name_path'] = 'jsonpath' |
|
50 |
+ @checker.should be_valid |
|
51 |
+ end |
|
52 |
+ |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ describe "#receive" do |
|
56 |
+ it "send a message to the hipchat" do |
|
57 |
+ any_instance_of(HipChat::Room) do |obj| |
|
58 |
+ mock(obj).send(@event.payload[:username], @event.payload[:message], {:notify => 0, :color => 'yellow'}) |
|
59 |
+ end |
|
60 |
+ @checker.receive([@event]) |
|
61 |
+ end |
|
62 |
+ end |
|
63 |
+ |
|
64 |
+ describe "#working?" do |
|
65 |
+ it "should not be working until the first event was received" do |
|
66 |
+ @checker.should_not be_working |
|
67 |
+ @checker.last_receive_at = Time.now |
|
68 |
+ @checker.should be_working |
|
69 |
+ end |
|
70 |
+ |
|
71 |
+ it "should not be working when the last error occured after the last received event" do |
|
72 |
+ @checker.last_receive_at = Time.now - 1.minute |
|
73 |
+ @checker.last_error_log_at = Time.now |
|
74 |
+ @checker.should_not be_working |
|
75 |
+ end |
|
76 |
+ |
|
77 |
+ it "should be working when the last received event occured after the last error" do |
|
78 |
+ @checker.last_receive_at = Time.now |
|
79 |
+ @checker.last_error_log_at = Time.now - 1.minute |
|
80 |
+ @checker.should be_working |
|
81 |
+ end |
|
82 |
+ end |
|
83 |
+end |
@@ -5,8 +5,11 @@ describe Agents::PostAgent do |
||
5 | 5 |
@valid_params = { |
6 | 6 |
:name => "somename", |
7 | 7 |
:options => { |
8 |
- :post_url => "http://www.example.com", |
|
9 |
- :expected_receive_period_in_days => 1 |
|
8 |
+ 'post_url' => "http://www.example.com", |
|
9 |
+ 'expected_receive_period_in_days' => 1, |
|
10 |
+ 'payload' => { |
|
11 |
+ 'default' => 'value' |
|
12 |
+ } |
|
10 | 13 |
} |
11 | 14 |
} |
12 | 15 |
|
@@ -17,28 +20,69 @@ describe Agents::PostAgent do |
||
17 | 20 |
@event = Event.new |
18 | 21 |
@event.agent = agents(:jane_weather_agent) |
19 | 22 |
@event.payload = { |
20 |
- :somekey => "somevalue", |
|
21 |
- :someotherkey => { |
|
22 |
- :somekey => "value" |
|
23 |
+ 'somekey' => 'somevalue', |
|
24 |
+ 'someotherkey' => { |
|
25 |
+ 'somekey' => 'value' |
|
23 | 26 |
} |
24 | 27 |
} |
25 | 28 |
|
26 |
- @sent_messages = [] |
|
27 |
- stub.any_instance_of(Agents::PostAgent).post_event { |uri, event| @sent_messages << event } |
|
29 |
+ @sent_posts = [] |
|
30 |
+ @sent_gets = [] |
|
31 |
+ stub.any_instance_of(Agents::PostAgent).post_data { |data| @sent_posts << data } |
|
32 |
+ stub.any_instance_of(Agents::PostAgent).get_data { |data| @sent_gets << data } |
|
28 | 33 |
end |
29 | 34 |
|
30 | 35 |
describe "#receive" do |
31 |
- it "checks if it can handle multiple events" do |
|
36 |
+ it "can handle multiple events and merge the payloads with options['payload']" do |
|
32 | 37 |
event1 = Event.new |
33 | 38 |
event1.agent = agents(:bob_weather_agent) |
34 | 39 |
event1.payload = { |
35 |
- :xyz => "value1", |
|
36 |
- :message => "value2" |
|
40 |
+ 'xyz' => 'value1', |
|
41 |
+ 'message' => 'value2', |
|
42 |
+ 'default' => 'value2' |
|
37 | 43 |
} |
38 | 44 |
|
39 | 45 |
lambda { |
40 |
- @checker.receive([@event, event1]) |
|
41 |
- }.should change { @sent_messages.length }.by(2) |
|
46 |
+ lambda { |
|
47 |
+ @checker.receive([@event, event1]) |
|
48 |
+ }.should change { @sent_posts.length }.by(2) |
|
49 |
+ }.should_not change { @sent_gets.length } |
|
50 |
+ |
|
51 |
+ @sent_posts[0].should == @event.payload.merge('default' => 'value') |
|
52 |
+ @sent_posts[1].should == event1.payload |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ it "can make GET requests" do |
|
56 |
+ @checker.options['method'] = 'get' |
|
57 |
+ |
|
58 |
+ lambda { |
|
59 |
+ lambda { |
|
60 |
+ @checker.receive([@event]) |
|
61 |
+ }.should change { @sent_gets.length }.by(1) |
|
62 |
+ }.should_not change { @sent_posts.length } |
|
63 |
+ |
|
64 |
+ @sent_gets[0].should == @event.payload.merge('default' => 'value') |
|
65 |
+ end |
|
66 |
+ end |
|
67 |
+ |
|
68 |
+ describe "#check" do |
|
69 |
+ it "sends options['payload'] as a POST request" do |
|
70 |
+ lambda { |
|
71 |
+ @checker.check |
|
72 |
+ }.should change { @sent_posts.length }.by(1) |
|
73 |
+ |
|
74 |
+ @sent_posts[0].should == @checker.options['payload'] |
|
75 |
+ end |
|
76 |
+ |
|
77 |
+ it "sends options['payload'] as a GET request" do |
|
78 |
+ @checker.options['method'] = 'get' |
|
79 |
+ lambda { |
|
80 |
+ lambda { |
|
81 |
+ @checker.check |
|
82 |
+ }.should change { @sent_gets.length }.by(1) |
|
83 |
+ }.should_not change { @sent_posts.length } |
|
84 |
+ |
|
85 |
+ @sent_gets[0].should == @checker.options['payload'] |
|
42 | 86 |
end |
43 | 87 |
end |
44 | 88 |
|
@@ -59,13 +103,82 @@ describe Agents::PostAgent do |
||
59 | 103 |
end |
60 | 104 |
|
61 | 105 |
it "should validate presence of post_url" do |
62 |
- @checker.options[:post_url] = "" |
|
106 |
+ @checker.options['post_url'] = "" |
|
63 | 107 |
@checker.should_not be_valid |
64 | 108 |
end |
65 | 109 |
|
66 | 110 |
it "should validate presence of expected_receive_period_in_days" do |
67 |
- @checker.options[:expected_receive_period_in_days] = "" |
|
111 |
+ @checker.options['expected_receive_period_in_days'] = "" |
|
68 | 112 |
@checker.should_not be_valid |
69 | 113 |
end |
114 |
+ |
|
115 |
+ it "should validate method as post or get, defaulting to post" do |
|
116 |
+ @checker.options['method'] = "" |
|
117 |
+ @checker.method.should == "post" |
|
118 |
+ @checker.should be_valid |
|
119 |
+ |
|
120 |
+ @checker.options['method'] = "POST" |
|
121 |
+ @checker.method.should == "post" |
|
122 |
+ @checker.should be_valid |
|
123 |
+ |
|
124 |
+ @checker.options['method'] = "get" |
|
125 |
+ @checker.method.should == "get" |
|
126 |
+ @checker.should be_valid |
|
127 |
+ |
|
128 |
+ @checker.options['method'] = "wut" |
|
129 |
+ @checker.method.should == "wut" |
|
130 |
+ @checker.should_not be_valid |
|
131 |
+ end |
|
132 |
+ |
|
133 |
+ it "should validate payload as a hash, if present" do |
|
134 |
+ @checker.options['payload'] = "" |
|
135 |
+ @checker.should be_valid |
|
136 |
+ |
|
137 |
+ @checker.options['payload'] = "hello" |
|
138 |
+ @checker.should_not be_valid |
|
139 |
+ |
|
140 |
+ @checker.options['payload'] = ["foo", "bar"] |
|
141 |
+ @checker.should_not be_valid |
|
142 |
+ |
|
143 |
+ @checker.options['payload'] = { 'this' => 'that' } |
|
144 |
+ @checker.should be_valid |
|
145 |
+ end |
|
146 |
+ |
|
147 |
+ it "requires headers to be a hash, if present" do |
|
148 |
+ @checker.options['headers'] = [1,2,3] |
|
149 |
+ @checker.should_not be_valid |
|
150 |
+ |
|
151 |
+ @checker.options['headers'] = "hello world" |
|
152 |
+ @checker.should_not be_valid |
|
153 |
+ |
|
154 |
+ @checker.options['headers'] = "" |
|
155 |
+ @checker.should be_valid |
|
156 |
+ |
|
157 |
+ @checker.options['headers'] = {} |
|
158 |
+ @checker.should be_valid |
|
159 |
+ |
|
160 |
+ @checker.options['headers'] = { "Authorization" => "foo bar" } |
|
161 |
+ @checker.should be_valid |
|
162 |
+ end |
|
163 |
+ end |
|
164 |
+ |
|
165 |
+ describe "#generate_uri" do |
|
166 |
+ it "merges params with any in the post_url" do |
|
167 |
+ @checker.options['post_url'] = "http://example.com/a/path?existing_param=existing_value" |
|
168 |
+ uri = @checker.generate_uri("some_param" => "some_value", "another_param" => "another_value") |
|
169 |
+ uri.request_uri.should == "/a/path?existing_param=existing_value&some_param=some_value&another_param=another_value" |
|
170 |
+ end |
|
171 |
+ |
|
172 |
+ it "works fine with urls that do not have a query" do |
|
173 |
+ @checker.options['post_url'] = "http://example.com/a/path" |
|
174 |
+ uri = @checker.generate_uri("some_param" => "some_value", "another_param" => "another_value") |
|
175 |
+ uri.request_uri.should == "/a/path?some_param=some_value&another_param=another_value" |
|
176 |
+ end |
|
177 |
+ |
|
178 |
+ it "just returns the post_uri when no params are given" do |
|
179 |
+ @checker.options['post_url'] = "http://example.com/a/path?existing_param=existing_value" |
|
180 |
+ uri = @checker.generate_uri |
|
181 |
+ uri.request_uri.should == "/a/path?existing_param=existing_value" |
|
182 |
+ end |
|
70 | 183 |
end |
71 | 184 |
end |
@@ -19,7 +19,6 @@ describe Agents::PublicTransportAgent do |
||
19 | 19 |
stub_request(:get, "http://webservices.nextbus.com/service/publicXMLFeed?a=sf-muni&command=predictionsForMultiStops&stops=N%7C5215"). |
20 | 20 |
with(:headers => {'User-Agent'=>'Typhoeus - https://github.com/typhoeus/typhoeus'}). |
21 | 21 |
to_return(:status => 200, :body => File.read(Rails.root.join("spec/data_fixtures/public_transport_agent.xml")), :headers => {}) |
22 |
- stub(Time).now {"2014-01-14 20:21:30 +0500".to_time} |
|
23 | 22 |
end |
24 | 23 |
|
25 | 24 |
it "should create 4 events" do |
@@ -27,15 +26,18 @@ describe Agents::PublicTransportAgent do |
||
27 | 26 |
end |
28 | 27 |
|
29 | 28 |
it "should add 4 items to memory" do |
30 |
- @agent.memory.should == {} |
|
31 |
- @agent.check |
|
32 |
- @agent.memory.should == {"existing_routes" => [ |
|
33 |
- {"stopTag"=>"5221", "tripTag"=>"5840324", "epochTime"=>"1389706393991", "currentTime"=>"2014-01-14 20:21:30 +0500"}, |
|
34 |
- {"stopTag"=>"5221", "tripTag"=>"5840083", "epochTime"=>"1389706512784", "currentTime"=>"2014-01-14 20:21:30 +0500"}, |
|
35 |
- {"stopTag"=>"5215", "tripTag"=>"5840324", "epochTime"=>"1389706282012", "currentTime"=>"2014-01-14 20:21:30 +0500"}, |
|
36 |
- {"stopTag"=>"5215", "tripTag"=>"5840083", "epochTime"=>"1389706400805", "currentTime"=>"2014-01-14 20:21:30 +0500"} |
|
37 |
- ] |
|
38 |
- } |
|
29 |
+ time_travel_to Time.parse("2014-01-14 20:21:30 +0500") do |
|
30 |
+ @agent.memory.should == {} |
|
31 |
+ @agent.check |
|
32 |
+ @agent.save |
|
33 |
+ @agent.reload.memory.should == {"existing_routes" => [ |
|
34 |
+ {"stopTag"=>"5221", "tripTag"=>"5840324", "epochTime"=>"1389706393991", "currentTime"=>Time.now.to_s}, |
|
35 |
+ {"stopTag"=>"5221", "tripTag"=>"5840083", "epochTime"=>"1389706512784", "currentTime"=>Time.now.to_s}, |
|
36 |
+ {"stopTag"=>"5215", "tripTag"=>"5840324", "epochTime"=>"1389706282012", "currentTime"=>Time.now.to_s}, |
|
37 |
+ {"stopTag"=>"5215", "tripTag"=>"5840083", "epochTime"=>"1389706400805", "currentTime"=>Time.now.to_s} |
|
38 |
+ ] |
|
39 |
+ } |
|
40 |
+ end |
|
39 | 41 |
end |
40 | 42 |
|
41 | 43 |
it "should not create events twice" do |
@@ -44,10 +46,13 @@ describe Agents::PublicTransportAgent do |
||
44 | 46 |
end |
45 | 47 |
|
46 | 48 |
it "should reset memory after 2 hours" do |
47 |
- lambda { @agent.check }.should change {@agent.events.count}.by(4) |
|
48 |
- stub(Time).now {"2014-01-14 20:21:30 +0500".to_time + 3.hours} |
|
49 |
- @agent.cleanup_old_memory |
|
50 |
- lambda { @agent.check }.should change {@agent.events.count}.by(4) |
|
49 |
+ time_travel_to Time.parse("2014-01-14 20:21:30 +0500") do |
|
50 |
+ lambda { @agent.check }.should change {@agent.events.count}.by(4) |
|
51 |
+ end |
|
52 |
+ time_travel_to "2014-01-14 23:21:30 +0500".to_time do |
|
53 |
+ @agent.cleanup_old_memory |
|
54 |
+ lambda { @agent.check }.should change {@agent.events.count}.by(4) |
|
55 |
+ end |
|
51 | 56 |
end |
52 | 57 |
end |
53 | 58 |
|
@@ -0,0 +1,80 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+require 'models/concerns/json_path_options_overwritable' |
|
3 |
+ |
|
4 |
+describe Agents::PushbulletAgent do |
|
5 |
+ it_behaves_like JsonPathOptionsOverwritable |
|
6 |
+ |
|
7 |
+ before(:each) do |
|
8 |
+ @valid_params = { |
|
9 |
+ 'api_key' => 'token', |
|
10 |
+ 'device_id' => '124', |
|
11 |
+ 'body_path' => '$.body', |
|
12 |
+ 'title' => 'hello from huginn' |
|
13 |
+ } |
|
14 |
+ |
|
15 |
+ @checker = Agents::PushbulletAgent.new(:name => "somename", :options => @valid_params) |
|
16 |
+ @checker.user = users(:jane) |
|
17 |
+ @checker.save! |
|
18 |
+ |
|
19 |
+ @event = Event.new |
|
20 |
+ @event.agent = agents(:bob_weather_agent) |
|
21 |
+ @event.payload = { :body => 'One two test' } |
|
22 |
+ @event.save! |
|
23 |
+ end |
|
24 |
+ |
|
25 |
+ describe "validating" do |
|
26 |
+ before do |
|
27 |
+ @checker.should be_valid |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+ it "should require the api_key" do |
|
31 |
+ @checker.options['api_key'] = nil |
|
32 |
+ @checker.should_not be_valid |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ it "should require the device_id" do |
|
36 |
+ @checker.options['device_id'] = nil |
|
37 |
+ @checker.should_not be_valid |
|
38 |
+ end |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ describe "helpers" do |
|
42 |
+ it "it should return the correct basic_options" do |
|
43 |
+ @checker.send(:basic_options).should == {:basic_auth => {:username =>@checker.options[:api_key], :password=>''}, |
|
44 |
+ :body => {:device_iden => @checker.options[:device_id], :type => 'note'}} |
|
45 |
+ end |
|
46 |
+ |
|
47 |
+ |
|
48 |
+ it "should return the query_options" do |
|
49 |
+ @checker.send(:query_options, @event).should == @checker.send(:basic_options).deep_merge({ |
|
50 |
+ :body => {:title => 'hello from huginn', :body => 'One two test'} |
|
51 |
+ }) |
|
52 |
+ end |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ describe "#receive" do |
|
56 |
+ it "send a message to the hipchat" do |
|
57 |
+ stub_request(:post, "https://token:@api.pushbullet.com/api/pushes"). |
|
58 |
+ with(:body => "device_iden=124&type=note&title=hello%20from%20huginn&body=One%20two%20test"). |
|
59 |
+ to_return(:status => 200, :body => "ok", :headers => {}) |
|
60 |
+ dont_allow(@checker).error |
|
61 |
+ @checker.receive([@event]) |
|
62 |
+ end |
|
63 |
+ |
|
64 |
+ it "should log resquests which return an error" do |
|
65 |
+ stub_request(:post, "https://token:@api.pushbullet.com/api/pushes"). |
|
66 |
+ with(:body => "device_iden=124&type=note&title=hello%20from%20huginn&body=One%20two%20test"). |
|
67 |
+ to_return(:status => 200, :body => "error", :headers => {}) |
|
68 |
+ mock(@checker).error("error") |
|
69 |
+ @checker.receive([@event]) |
|
70 |
+ end |
|
71 |
+ end |
|
72 |
+ |
|
73 |
+ describe "#working?" do |
|
74 |
+ it "should not be working until the first event was received" do |
|
75 |
+ @checker.should_not be_working |
|
76 |
+ @checker.last_receive_at = Time.now |
|
77 |
+ @checker.should be_working |
|
78 |
+ end |
|
79 |
+ end |
|
80 |
+end |
@@ -0,0 +1,222 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Agents::PushoverAgent do |
|
4 |
+ before do |
|
5 |
+ @checker = Agents::PushoverAgent.new(:name => 'Some Name', |
|
6 |
+ :options => { :token => 'x', |
|
7 |
+ :user => 'x', |
|
8 |
+ :message => 'Some Message', |
|
9 |
+ :device => 'Some Device', |
|
10 |
+ :title => 'Some Message Title', |
|
11 |
+ :url => 'http://someurl.com', |
|
12 |
+ :url_title => 'Some Url Title', |
|
13 |
+ :priority => 0, |
|
14 |
+ :timestamp => 'false', |
|
15 |
+ :sound => 'pushover', |
|
16 |
+ :retry => 0, |
|
17 |
+ :expire => 0, |
|
18 |
+ :expected_receive_period_in_days => '1'}) |
|
19 |
+ |
|
20 |
+ @checker.user = users(:bob) |
|
21 |
+ @checker.save! |
|
22 |
+ |
|
23 |
+ @event = Event.new |
|
24 |
+ @event.agent = agents(:bob_weather_agent) |
|
25 |
+ @event.payload = { :message => 'Looks like its going to rain' } |
|
26 |
+ @event.save! |
|
27 |
+ |
|
28 |
+ @sent_notifications = [] |
|
29 |
+ stub.any_instance_of(Agents::PushoverAgent).send_notification { |notification| @sent_notifications << notification} |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ describe '#receive' do |
|
33 |
+ it 'should make sure multiple events are being received' do |
|
34 |
+ event1 = Event.new |
|
35 |
+ event1.agent = agents(:bob_rain_notifier_agent) |
|
36 |
+ event1.payload = { :message => 'Some message' } |
|
37 |
+ event1.save! |
|
38 |
+ |
|
39 |
+ event2 = Event.new |
|
40 |
+ event2.agent = agents(:bob_weather_agent) |
|
41 |
+ event2.payload = { :message => 'Some other message' } |
|
42 |
+ event2.save! |
|
43 |
+ |
|
44 |
+ @checker.receive([@event,event1,event2]) |
|
45 |
+ @sent_notifications[0]['message'].should == 'Looks like its going to rain' |
|
46 |
+ @sent_notifications[1]['message'].should == 'Some message' |
|
47 |
+ @sent_notifications[2]['message'].should == 'Some other message' |
|
48 |
+ end |
|
49 |
+ |
|
50 |
+ it 'should make sure event message overrides default message' do |
|
51 |
+ event = Event.new |
|
52 |
+ event.agent = agents(:bob_rain_notifier_agent) |
|
53 |
+ event.payload = { :message => 'Some new message'} |
|
54 |
+ event.save! |
|
55 |
+ |
|
56 |
+ @checker.receive([event]) |
|
57 |
+ @sent_notifications[0]['message'].should == 'Some new message' |
|
58 |
+ end |
|
59 |
+ |
|
60 |
+ it 'should make sure event text overrides default message' do |
|
61 |
+ event = Event.new |
|
62 |
+ event.agent = agents(:bob_rain_notifier_agent) |
|
63 |
+ event.payload = { :text => 'Some new text'} |
|
64 |
+ event.save! |
|
65 |
+ |
|
66 |
+ @checker.receive([event]) |
|
67 |
+ @sent_notifications[0]['message'].should == 'Some new text' |
|
68 |
+ end |
|
69 |
+ |
|
70 |
+ it 'should make sure event title overrides default title' do |
|
71 |
+ event = Event.new |
|
72 |
+ event.agent = agents(:bob_rain_notifier_agent) |
|
73 |
+ event.payload = { :message => 'Some message', :title => 'Some new title' } |
|
74 |
+ event.save! |
|
75 |
+ |
|
76 |
+ @checker.receive([event]) |
|
77 |
+ @sent_notifications[0]['title'].should == 'Some new title' |
|
78 |
+ end |
|
79 |
+ |
|
80 |
+ it 'should make sure event url overrides default url' do |
|
81 |
+ event = Event.new |
|
82 |
+ event.agent = agents(:bob_rain_notifier_agent) |
|
83 |
+ event.payload = { :message => 'Some message', :url => 'Some new url' } |
|
84 |
+ event.save! |
|
85 |
+ |
|
86 |
+ @checker.receive([event]) |
|
87 |
+ @sent_notifications[0]['url'].should == 'Some new url' |
|
88 |
+ end |
|
89 |
+ |
|
90 |
+ it 'should make sure event url_title overrides default url_title' do |
|
91 |
+ event = Event.new |
|
92 |
+ event.agent = agents(:bob_rain_notifier_agent) |
|
93 |
+ event.payload = { :message => 'Some message', :url_title => 'Some new url_title' } |
|
94 |
+ event.save! |
|
95 |
+ |
|
96 |
+ @checker.receive([event]) |
|
97 |
+ @sent_notifications[0]['url_title'].should == 'Some new url_title' |
|
98 |
+ end |
|
99 |
+ |
|
100 |
+ it 'should make sure event priority overrides default priority' do |
|
101 |
+ event = Event.new |
|
102 |
+ event.agent = agents(:bob_rain_notifier_agent) |
|
103 |
+ event.payload = { :message => 'Some message', :priority => 1 } |
|
104 |
+ event.save! |
|
105 |
+ |
|
106 |
+ @checker.receive([event]) |
|
107 |
+ @sent_notifications[0]['priority'].should == 1 |
|
108 |
+ end |
|
109 |
+ |
|
110 |
+ it 'should make sure event timestamp overrides default timestamp' do |
|
111 |
+ event = Event.new |
|
112 |
+ event.agent = agents(:bob_rain_notifier_agent) |
|
113 |
+ event.payload = { :message => 'Some message', :timestamp => 'false' } |
|
114 |
+ event.save! |
|
115 |
+ |
|
116 |
+ @checker.receive([event]) |
|
117 |
+ @sent_notifications[0]['timestamp'].should == 'false' |
|
118 |
+ end |
|
119 |
+ |
|
120 |
+ it 'should make sure event sound overrides default sound' do |
|
121 |
+ event = Event.new |
|
122 |
+ event.agent = agents(:bob_rain_notifier_agent) |
|
123 |
+ event.payload = { :message => 'Some message', :sound => 'Some new sound' } |
|
124 |
+ event.save! |
|
125 |
+ |
|
126 |
+ @checker.receive([event]) |
|
127 |
+ @sent_notifications[0]['sound'].should == 'Some new sound' |
|
128 |
+ end |
|
129 |
+ |
|
130 |
+ it 'should make sure event retry overrides default retry' do |
|
131 |
+ event = Event.new |
|
132 |
+ event.agent = agents(:bob_rain_notifier_agent) |
|
133 |
+ event.payload = { :message => 'Some message', :retry => 1 } |
|
134 |
+ event.save! |
|
135 |
+ |
|
136 |
+ @checker.receive([event]) |
|
137 |
+ @sent_notifications[0]['retry'].should == 1 |
|
138 |
+ end |
|
139 |
+ |
|
140 |
+ it 'should make sure event expire overrides default expire' do |
|
141 |
+ event = Event.new |
|
142 |
+ event.agent = agents(:bob_rain_notifier_agent) |
|
143 |
+ event.payload = { :message => 'Some message', :expire => 60 } |
|
144 |
+ event.save! |
|
145 |
+ |
|
146 |
+ @checker.receive([event]) |
|
147 |
+ @sent_notifications[0]['expire'].should == 60 |
|
148 |
+ end |
|
149 |
+ end |
|
150 |
+ |
|
151 |
+ describe '#working?' do |
|
152 |
+ it 'checks if events have been received within the expected receive period' do |
|
153 |
+ # No events received |
|
154 |
+ @checker.should_not be_working |
|
155 |
+ Agents::PushoverAgent.async_receive @checker.id, [@event.id] |
|
156 |
+ |
|
157 |
+ # Just received events |
|
158 |
+ @checker.reload.should be_working |
|
159 |
+ two_days_from_now = 2.days.from_now |
|
160 |
+ stub(Time).now { two_days_from_now } |
|
161 |
+ |
|
162 |
+ # More time has passed than the expected receive period without any new events |
|
163 |
+ @checker.reload.should_not be_working |
|
164 |
+ end |
|
165 |
+ end |
|
166 |
+ |
|
167 |
+ describe "validation" do |
|
168 |
+ before do |
|
169 |
+ @checker.should be_valid |
|
170 |
+ end |
|
171 |
+ |
|
172 |
+ it "should validate presence of token" do |
|
173 |
+ @checker.options[:token] = "" |
|
174 |
+ @checker.should_not be_valid |
|
175 |
+ end |
|
176 |
+ |
|
177 |
+ it "should validate presence of user" do |
|
178 |
+ @checker.options[:user] = "" |
|
179 |
+ @checker.should_not be_valid |
|
180 |
+ end |
|
181 |
+ |
|
182 |
+ it "should validate presence of expected_receive_period_in_days" do |
|
183 |
+ @checker.options[:expected_receive_period_in_days] = "" |
|
184 |
+ @checker.should_not be_valid |
|
185 |
+ end |
|
186 |
+ |
|
187 |
+ it "should make sure device is optional" do |
|
188 |
+ @checker.options[:device] = "" |
|
189 |
+ @checker.should be_valid |
|
190 |
+ end |
|
191 |
+ |
|
192 |
+ it "should make sure title is optional" do |
|
193 |
+ @checker.options[:title] = "" |
|
194 |
+ @checker.should be_valid |
|
195 |
+ end |
|
196 |
+ |
|
197 |
+ it "should make sure url is optional" do |
|
198 |
+ @checker.options[:url] = "" |
|
199 |
+ @checker.should be_valid |
|
200 |
+ end |
|
201 |
+ |
|
202 |
+ it "should make sure url_title is optional" do |
|
203 |
+ @checker.options[:url_title] = "" |
|
204 |
+ @checker.should be_valid |
|
205 |
+ end |
|
206 |
+ |
|
207 |
+ it "should make sure priority is optional" do |
|
208 |
+ @checker.options[:priority] = "" |
|
209 |
+ @checker.should be_valid |
|
210 |
+ end |
|
211 |
+ |
|
212 |
+ it "should make sure timestamp is optional" do |
|
213 |
+ @checker.options[:timestamp] = "" |
|
214 |
+ @checker.should be_valid |
|
215 |
+ end |
|
216 |
+ |
|
217 |
+ it "should make sure sound is optional" do |
|
218 |
+ @checker.options[:sound] = "" |
|
219 |
+ @checker.should be_valid |
|
220 |
+ end |
|
221 |
+ end |
|
222 |
+end |
@@ -0,0 +1,99 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Agents::ShellCommandAgent do |
|
4 |
+ before do |
|
5 |
+ @valid_path = Dir.pwd |
|
6 |
+ |
|
7 |
+ @valid_params = { |
|
8 |
+ :path => @valid_path, |
|
9 |
+ :command => "pwd", |
|
10 |
+ :expected_update_period_in_days => "1", |
|
11 |
+ } |
|
12 |
+ |
|
13 |
+ @checker = Agents::ShellCommandAgent.new(:name => "somename", :options => @valid_params) |
|
14 |
+ @checker.user = users(:jane) |
|
15 |
+ @checker.save! |
|
16 |
+ |
|
17 |
+ @event = Event.new |
|
18 |
+ @event.agent = agents(:jane_weather_agent) |
|
19 |
+ @event.payload = { |
|
20 |
+ :command => "ls" |
|
21 |
+ } |
|
22 |
+ @event.save! |
|
23 |
+ |
|
24 |
+ stub(Agents::ShellCommandAgent).should_run? { true } |
|
25 |
+ end |
|
26 |
+ |
|
27 |
+ describe "validation" do |
|
28 |
+ before do |
|
29 |
+ @checker.should be_valid |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ it "should validate presence of necessary fields" do |
|
33 |
+ @checker.options[:command] = nil |
|
34 |
+ @checker.should_not be_valid |
|
35 |
+ end |
|
36 |
+ |
|
37 |
+ it "should validate path" do |
|
38 |
+ @checker.options[:path] = 'notarealpath/itreallyisnt' |
|
39 |
+ @checker.should_not be_valid |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ it "should validate path" do |
|
43 |
+ @checker.options[:path] = '/' |
|
44 |
+ @checker.should be_valid |
|
45 |
+ end |
|
46 |
+ end |
|
47 |
+ |
|
48 |
+ describe "#working?" do |
|
49 |
+ it "generating events as scheduled" do |
|
50 |
+ stub(@checker).run_command(@valid_path, 'pwd') { ["fake pwd output", "", 0] } |
|
51 |
+ |
|
52 |
+ @checker.should_not be_working |
|
53 |
+ @checker.check |
|
54 |
+ @checker.reload.should be_working |
|
55 |
+ three_days_from_now = 3.days.from_now |
|
56 |
+ stub(Time).now { three_days_from_now } |
|
57 |
+ @checker.should_not be_working |
|
58 |
+ end |
|
59 |
+ end |
|
60 |
+ |
|
61 |
+ describe "#check" do |
|
62 |
+ before do |
|
63 |
+ stub(@checker).run_command(@valid_path, 'pwd') { ["fake pwd output", "", 0] } |
|
64 |
+ end |
|
65 |
+ |
|
66 |
+ it "should create an event when checking" do |
|
67 |
+ expect { @checker.check }.to change { Event.count }.by(1) |
|
68 |
+ Event.last.payload[:path].should == @valid_path |
|
69 |
+ Event.last.payload[:command].should == 'pwd' |
|
70 |
+ Event.last.payload[:output].should == "fake pwd output" |
|
71 |
+ end |
|
72 |
+ |
|
73 |
+ it "does not run when should_run? is false" do |
|
74 |
+ stub(Agents::ShellCommandAgent).should_run? { false } |
|
75 |
+ expect { @checker.check }.not_to change { Event.count } |
|
76 |
+ end |
|
77 |
+ end |
|
78 |
+ |
|
79 |
+ describe "#receive" do |
|
80 |
+ before do |
|
81 |
+ stub(@checker).run_command(@valid_path, @event.payload[:command]) { ["fake ls output", "", 0] } |
|
82 |
+ end |
|
83 |
+ |
|
84 |
+ it "creates events" do |
|
85 |
+ @checker.receive([@event]) |
|
86 |
+ Event.last.payload[:path].should == @valid_path |
|
87 |
+ Event.last.payload[:command].should == @event.payload[:command] |
|
88 |
+ Event.last.payload[:output].should == "fake ls output" |
|
89 |
+ end |
|
90 |
+ |
|
91 |
+ it "does not run when should_run? is false" do |
|
92 |
+ stub(Agents::ShellCommandAgent).should_run? { false } |
|
93 |
+ |
|
94 |
+ expect { |
|
95 |
+ @checker.receive([@event]) |
|
96 |
+ }.not_to change { Event.count } |
|
97 |
+ end |
|
98 |
+ end |
|
99 |
+end |
@@ -0,0 +1,67 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Agents::StubhubAgent do |
|
4 |
+ |
|
5 |
+ let(:name) { 'Agent Name' } |
|
6 |
+ let(:url) { 'http://www.stubhub.com/event/name-1-1-2014-12345' } |
|
7 |
+ let(:parsed_body) { JSON.parse(body)['response']['docs'][0] } |
|
8 |
+ let(:valid_params) { { 'url' => parsed_body['url'] } } |
|
9 |
+ let(:body) { File.read(Rails.root.join('spec/data_fixtures/stubhub_data.json')) } |
|
10 |
+ let(:stubhub_event_id) { 12345 } |
|
11 |
+ let(:response_payload) { { |
|
12 |
+ 'url' => url, |
|
13 |
+ 'name' => parsed_body['seo_description_en_US'], |
|
14 |
+ 'date' => parsed_body['event_date_local'], |
|
15 |
+ 'max_price' => parsed_body['maxPrice'], |
|
16 |
+ 'min_price' => parsed_body['minPrice'], |
|
17 |
+ 'total_postings' => parsed_body['totalPostings'], |
|
18 |
+ 'total_tickets' => parsed_body['totalTickets'], |
|
19 |
+ 'venue_name' => parsed_body['venue_name'] |
|
20 |
+ } } |
|
21 |
+ |
|
22 |
+ before do |
|
23 |
+ stub_request(:get, "http://www.stubhub.com/listingCatalog/select/?q=%2B%20stubhubDocumentType:event%0D%0A%2B%20event_id:#{stubhub_event_id}%0D%0A&rows=10&start=0&wt=json"). |
|
24 |
+ to_return(:status => 200, :body => body, :headers => {}) |
|
25 |
+ |
|
26 |
+ @stubhub_agent = described_class.new(name: name, options: valid_params) |
|
27 |
+ @stubhub_agent.user = users(:jane) |
|
28 |
+ @stubhub_agent.save! |
|
29 |
+ end |
|
30 |
+ |
|
31 |
+ |
|
32 |
+ describe "#check" do |
|
33 |
+ |
|
34 |
+ it 'should create an event' do |
|
35 |
+ expect { @stubhub_agent.check }.to change { Event.count }.by(1) |
|
36 |
+ end |
|
37 |
+ |
|
38 |
+ it 'should properly parse the response' do |
|
39 |
+ event = @stubhub_agent.check |
|
40 |
+ event.payload.should == response_payload |
|
41 |
+ end |
|
42 |
+ end |
|
43 |
+ |
|
44 |
+ describe "validations" do |
|
45 |
+ before do |
|
46 |
+ @stubhub_agent.should be_valid |
|
47 |
+ end |
|
48 |
+ |
|
49 |
+ it "should require a url" do |
|
50 |
+ @stubhub_agent.options['url'] = nil |
|
51 |
+ @stubhub_agent.should_not be_valid |
|
52 |
+ end |
|
53 |
+ |
|
54 |
+ end |
|
55 |
+ |
|
56 |
+ describe "#working?" do |
|
57 |
+ it "checks if events have been received within the expected receive period" do |
|
58 |
+ @stubhub_agent.should_not be_working |
|
59 |
+ |
|
60 |
+ Agents::StubhubAgent.async_check @stubhub_agent.id |
|
61 |
+ @stubhub_agent.reload.should be_working |
|
62 |
+ two_days_from_now = 2.days.from_now |
|
63 |
+ stub(Time).now { two_days_from_now } |
|
64 |
+ @stubhub_agent.reload.should_not be_working |
|
65 |
+ end |
|
66 |
+ end |
|
67 |
+end |
@@ -30,9 +30,32 @@ describe Agents::TriggerAgent do |
||
30 | 30 |
@checker.should be_valid |
31 | 31 |
end |
32 | 32 |
|
33 |
- it "should validate presence of options" do |
|
33 |
+ it "should validate presence of message" do |
|
34 | 34 |
@checker.options['message'] = nil |
35 | 35 |
@checker.should_not be_valid |
36 |
+ |
|
37 |
+ @checker.options['message'] = '' |
|
38 |
+ @checker.should_not be_valid |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ it "should be valid without a message when 'keep_event' is set" do |
|
42 |
+ @checker.options['keep_event'] = 'true' |
|
43 |
+ @checker.options['message'] = '' |
|
44 |
+ @checker.should be_valid |
|
45 |
+ end |
|
46 |
+ |
|
47 |
+ it "if present, 'keep_event' must equal true or false" do |
|
48 |
+ @checker.options['keep_event'] = 'true' |
|
49 |
+ @checker.should be_valid |
|
50 |
+ |
|
51 |
+ @checker.options['keep_event'] = 'false' |
|
52 |
+ @checker.should be_valid |
|
53 |
+ |
|
54 |
+ @checker.options['keep_event'] = '' |
|
55 |
+ @checker.should be_valid |
|
56 |
+ |
|
57 |
+ @checker.options['keep_event'] = 'tralse' |
|
58 |
+ @checker.should_not be_valid |
|
36 | 59 |
end |
37 | 60 |
|
38 | 61 |
it "should validate the three fields in each rule" do |
@@ -71,6 +94,28 @@ describe Agents::TriggerAgent do |
||
71 | 94 |
}.should change { Event.count }.by(1) |
72 | 95 |
end |
73 | 96 |
|
97 |
+ it "handles array of regex" do |
|
98 |
+ @event.payload['foo']['bar']['baz'] = "a222b" |
|
99 |
+ @checker.options['rules'][0] = { |
|
100 |
+ 'type' => "regex", |
|
101 |
+ 'value' => ["a\\db", "a\\Wb"], |
|
102 |
+ 'path' => "foo.bar.baz", |
|
103 |
+ } |
|
104 |
+ lambda { |
|
105 |
+ @checker.receive([@event]) |
|
106 |
+ }.should_not change { Event.count } |
|
107 |
+ |
|
108 |
+ @event.payload['foo']['bar']['baz'] = "a2b" |
|
109 |
+ lambda { |
|
110 |
+ @checker.receive([@event]) |
|
111 |
+ }.should change { Event.count }.by(1) |
|
112 |
+ |
|
113 |
+ @event.payload['foo']['bar']['baz'] = "a b" |
|
114 |
+ lambda { |
|
115 |
+ @checker.receive([@event]) |
|
116 |
+ }.should change { Event.count }.by(1) |
|
117 |
+ end |
|
118 |
+ |
|
74 | 119 |
it "handles negated regex" do |
75 | 120 |
@event.payload['foo']['bar']['baz'] = "a2b" |
76 | 121 |
@checker.options['rules'][0] = { |
@@ -89,6 +134,24 @@ describe Agents::TriggerAgent do |
||
89 | 134 |
}.should change { Event.count }.by(1) |
90 | 135 |
end |
91 | 136 |
|
137 |
+ it "handles array of negated regex" do |
|
138 |
+ @event.payload['foo']['bar']['baz'] = "a2b" |
|
139 |
+ @checker.options['rules'][0] = { |
|
140 |
+ 'type' => "!regex", |
|
141 |
+ 'value' => ["a\\db", "a2b"], |
|
142 |
+ 'path' => "foo.bar.baz", |
|
143 |
+ } |
|
144 |
+ |
|
145 |
+ lambda { |
|
146 |
+ @checker.receive([@event]) |
|
147 |
+ }.should_not change { Event.count } |
|
148 |
+ |
|
149 |
+ @event.payload['foo']['bar']['baz'] = "a3b" |
|
150 |
+ lambda { |
|
151 |
+ @checker.receive([@event]) |
|
152 |
+ }.should change { Event.count }.by(1) |
|
153 |
+ end |
|
154 |
+ |
|
92 | 155 |
it "puts can extract values into the message based on paths" do |
93 | 156 |
@checker.receive([@event]) |
94 | 157 |
Event.last.payload['message'].should == "I saw 'a2b' from Joe" |
@@ -109,6 +172,21 @@ describe Agents::TriggerAgent do |
||
109 | 172 |
}.should_not change { Event.count } |
110 | 173 |
end |
111 | 174 |
|
175 |
+ it "handles array of numerical comparisons" do |
|
176 |
+ @event.payload['foo']['bar']['baz'] = "5" |
|
177 |
+ @checker.options['rules'].first['value'] = [6, 3] |
|
178 |
+ @checker.options['rules'].first['type'] = "field<value" |
|
179 |
+ |
|
180 |
+ lambda { |
|
181 |
+ @checker.receive([@event]) |
|
182 |
+ }.should change { Event.count }.by(1) |
|
183 |
+ |
|
184 |
+ @checker.options['rules'].first['value'] = [4, 3] |
|
185 |
+ lambda { |
|
186 |
+ @checker.receive([@event]) |
|
187 |
+ }.should_not change { Event.count } |
|
188 |
+ end |
|
189 |
+ |
|
112 | 190 |
it "handles exact comparisons" do |
113 | 191 |
@event.payload['foo']['bar']['baz'] = "hello world" |
114 | 192 |
@checker.options['rules'].first['type'] = "field==value" |
@@ -124,6 +202,21 @@ describe Agents::TriggerAgent do |
||
124 | 202 |
}.should change { Event.count }.by(1) |
125 | 203 |
end |
126 | 204 |
|
205 |
+ it "handles array of exact comparisons" do |
|
206 |
+ @event.payload['foo']['bar']['baz'] = "hello world" |
|
207 |
+ @checker.options['rules'].first['type'] = "field==value" |
|
208 |
+ |
|
209 |
+ @checker.options['rules'].first['value'] = ["hello there", "hello universe"] |
|
210 |
+ lambda { |
|
211 |
+ @checker.receive([@event]) |
|
212 |
+ }.should_not change { Event.count } |
|
213 |
+ |
|
214 |
+ @checker.options['rules'].first['value'] = ["hello world", "hello universe"] |
|
215 |
+ lambda { |
|
216 |
+ @checker.receive([@event]) |
|
217 |
+ }.should change { Event.count }.by(1) |
|
218 |
+ end |
|
219 |
+ |
|
127 | 220 |
it "handles negated comparisons" do |
128 | 221 |
@event.payload['foo']['bar']['baz'] = "hello world" |
129 | 222 |
@checker.options['rules'].first['type'] = "field!=value" |
@@ -140,6 +233,22 @@ describe Agents::TriggerAgent do |
||
140 | 233 |
}.should change { Event.count }.by(1) |
141 | 234 |
end |
142 | 235 |
|
236 |
+ it "handles array of negated comparisons" do |
|
237 |
+ @event.payload['foo']['bar']['baz'] = "hello world" |
|
238 |
+ @checker.options['rules'].first['type'] = "field!=value" |
|
239 |
+ @checker.options['rules'].first['value'] = ["hello world", "hello world"] |
|
240 |
+ |
|
241 |
+ lambda { |
|
242 |
+ @checker.receive([@event]) |
|
243 |
+ }.should_not change { Event.count } |
|
244 |
+ |
|
245 |
+ @checker.options['rules'].first['value'] = ["hello there", "hello world"] |
|
246 |
+ |
|
247 |
+ lambda { |
|
248 |
+ @checker.receive([@event]) |
|
249 |
+ }.should change { Event.count }.by(1) |
|
250 |
+ end |
|
251 |
+ |
|
143 | 252 |
it "does fine without dots in the path" do |
144 | 253 |
@event.payload = { 'hello' => "world" } |
145 | 254 |
@checker.options['rules'].first['type'] = "field==value" |
@@ -192,5 +301,38 @@ describe Agents::TriggerAgent do |
||
192 | 301 |
@checker.receive([@event]) |
193 | 302 |
}.should_not change { Event.count } |
194 | 303 |
end |
304 |
+ |
|
305 |
+ describe "when 'keep_event' is true" do |
|
306 |
+ before do |
|
307 |
+ @checker.options['keep_event'] = 'true' |
|
308 |
+ @event.payload['foo']['bar']['baz'] = "5" |
|
309 |
+ @checker.options['rules'].first['type'] = "field<value" |
|
310 |
+ end |
|
311 |
+ |
|
312 |
+ it "can re-emit the origin event" do |
|
313 |
+ @checker.options['rules'].first['value'] = 3 |
|
314 |
+ @checker.options['message'] = '' |
|
315 |
+ @event.payload['message'] = 'hi there' |
|
316 |
+ |
|
317 |
+ lambda { |
|
318 |
+ @checker.receive([@event]) |
|
319 |
+ }.should_not change { Event.count } |
|
320 |
+ |
|
321 |
+ @checker.options['rules'].first['value'] = 6 |
|
322 |
+ lambda { |
|
323 |
+ @checker.receive([@event]) |
|
324 |
+ }.should change { Event.count }.by(1) |
|
325 |
+ |
|
326 |
+ @checker.most_recent_event.payload.should == @event.payload |
|
327 |
+ end |
|
328 |
+ |
|
329 |
+ it "merges 'message' into the original event when present" do |
|
330 |
+ @checker.options['rules'].first['value'] = 6 |
|
331 |
+ |
|
332 |
+ @checker.receive([@event]) |
|
333 |
+ |
|
334 |
+ @checker.most_recent_event.payload.should == @event.payload.merge(:message => "I saw '5' from Joe") |
|
335 |
+ end |
|
336 |
+ end |
|
195 | 337 |
end |
196 | 338 |
end |
@@ -21,18 +21,71 @@ describe Agents::WebsiteAgent do |
||
21 | 21 |
@checker.save! |
22 | 22 |
end |
23 | 23 |
|
24 |
- describe "#check" do |
|
24 |
+ describe "validations" do |
|
25 |
+ before do |
|
26 |
+ @checker.should be_valid |
|
27 |
+ end |
|
28 |
+ |
|
25 | 29 |
it "should validate the integer fields" do |
26 |
- @checker.options['expected_update_period_in_days'] = "nonsense" |
|
27 |
- lambda { @checker.save! }.should raise_error; |
|
28 | 30 |
@checker.options['expected_update_period_in_days'] = "2" |
31 |
+ @checker.should be_valid |
|
32 |
+ |
|
33 |
+ @checker.options['expected_update_period_in_days'] = "nonsense" |
|
34 |
+ @checker.should_not be_valid |
|
35 |
+ end |
|
36 |
+ |
|
37 |
+ it "should validate uniqueness_look_back" do |
|
29 | 38 |
@checker.options['uniqueness_look_back'] = "nonsense" |
30 |
- lambda { @checker.save! }.should raise_error; |
|
39 |
+ @checker.should_not be_valid |
|
40 |
+ |
|
41 |
+ @checker.options['uniqueness_look_back'] = "2" |
|
42 |
+ @checker.should be_valid |
|
43 |
+ end |
|
44 |
+ |
|
45 |
+ it "should validate headers" do |
|
46 |
+ @checker.options['headers'] = "blah" |
|
47 |
+ @checker.should_not be_valid |
|
48 |
+ |
|
49 |
+ @checker.options['headers'] = "" |
|
50 |
+ @checker.should be_valid |
|
51 |
+ |
|
52 |
+ @checker.options['headers'] = {} |
|
53 |
+ @checker.should be_valid |
|
54 |
+ |
|
55 |
+ @checker.options['headers'] = { 'foo' => 'bar' } |
|
56 |
+ @checker.should be_valid |
|
57 |
+ end |
|
58 |
+ |
|
59 |
+ it "should validate mode" do |
|
31 | 60 |
@checker.options['mode'] = "nonsense" |
32 |
- lambda { @checker.save! }.should raise_error; |
|
33 |
- @checker.options = @site |
|
61 |
+ @checker.should_not be_valid |
|
62 |
+ |
|
63 |
+ @checker.options['mode'] = "on_change" |
|
64 |
+ @checker.should be_valid |
|
65 |
+ |
|
66 |
+ @checker.options['mode'] = "all" |
|
67 |
+ @checker.should be_valid |
|
68 |
+ |
|
69 |
+ @checker.options['mode'] = "" |
|
70 |
+ @checker.should be_valid |
|
71 |
+ end |
|
72 |
+ |
|
73 |
+ it "should validate the force_encoding option" do |
|
74 |
+ @checker.options['force_encoding'] = '' |
|
75 |
+ @checker.should be_valid |
|
76 |
+ |
|
77 |
+ @checker.options['force_encoding'] = 'UTF-8' |
|
78 |
+ @checker.should be_valid |
|
79 |
+ |
|
80 |
+ @checker.options['force_encoding'] = ['UTF-8'] |
|
81 |
+ @checker.should_not be_valid |
|
82 |
+ |
|
83 |
+ @checker.options['force_encoding'] = 'UTF-42' |
|
84 |
+ @checker.should_not be_valid |
|
34 | 85 |
end |
35 |
- |
|
86 |
+ end |
|
87 |
+ |
|
88 |
+ describe "#check" do |
|
36 | 89 |
it "should check for changes (and update Event.expires_at)" do |
37 | 90 |
lambda { @checker.check }.should change { Event.count }.by(1) |
38 | 91 |
event = Event.last |
@@ -81,6 +134,86 @@ describe Agents::WebsiteAgent do |
||
81 | 134 |
@checker.check |
82 | 135 |
@checker.logs.first.message.should =~ /Got an uneven number of matches/ |
83 | 136 |
end |
137 |
+ |
|
138 |
+ it "should accept an array for url" do |
|
139 |
+ @site['url'] = ["http://xkcd.com/1/", "http://xkcd.com/2/"] |
|
140 |
+ @checker.options = @site |
|
141 |
+ lambda { @checker.save! }.should_not raise_error; |
|
142 |
+ lambda { @checker.check }.should_not raise_error; |
|
143 |
+ end |
|
144 |
+ |
|
145 |
+ it "should parse events from all urls in array" do |
|
146 |
+ lambda { |
|
147 |
+ @site['url'] = ["http://xkcd.com/", "http://xkcd.com/"] |
|
148 |
+ @site['mode'] = 'all' |
|
149 |
+ @checker.options = @site |
|
150 |
+ @checker.check |
|
151 |
+ }.should change { Event.count }.by(2) |
|
152 |
+ end |
|
153 |
+ |
|
154 |
+ it "should follow unique rules when parsing array of urls" do |
|
155 |
+ lambda { |
|
156 |
+ @site['url'] = ["http://xkcd.com/", "http://xkcd.com/"] |
|
157 |
+ @checker.options = @site |
|
158 |
+ @checker.check |
|
159 |
+ }.should change { Event.count }.by(1) |
|
160 |
+ end |
|
161 |
+ end |
|
162 |
+ |
|
163 |
+ describe 'encoding' do |
|
164 |
+ it 'should be forced with force_encoding option' do |
|
165 |
+ huginn = "\u{601d}\u{8003}" |
|
166 |
+ stub_request(:any, /no-encoding/).to_return(:body => { |
|
167 |
+ :value => huginn, |
|
168 |
+ }.to_json.encode(Encoding::EUC_JP), :headers => { |
|
169 |
+ 'Content-Type' => 'application/json', |
|
170 |
+ }, :status => 200) |
|
171 |
+ site = { |
|
172 |
+ 'name' => "Some JSON Response", |
|
173 |
+ 'expected_update_period_in_days' => 2, |
|
174 |
+ 'type' => "json", |
|
175 |
+ 'url' => "http://no-encoding.example.com", |
|
176 |
+ 'mode' => 'on_change', |
|
177 |
+ 'extract' => { |
|
178 |
+ 'value' => { 'path' => 'value' }, |
|
179 |
+ }, |
|
180 |
+ 'force_encoding' => 'EUC-JP', |
|
181 |
+ } |
|
182 |
+ checker = Agents::WebsiteAgent.new(:name => "No Encoding Site", :options => site) |
|
183 |
+ checker.user = users(:bob) |
|
184 |
+ checker.save! |
|
185 |
+ |
|
186 |
+ checker.check |
|
187 |
+ event = Event.last |
|
188 |
+ event.payload['value'].should == huginn |
|
189 |
+ end |
|
190 |
+ |
|
191 |
+ it 'should be overridden with force_encoding option' do |
|
192 |
+ huginn = "\u{601d}\u{8003}" |
|
193 |
+ stub_request(:any, /wrong-encoding/).to_return(:body => { |
|
194 |
+ :value => huginn, |
|
195 |
+ }.to_json.encode(Encoding::EUC_JP), :headers => { |
|
196 |
+ 'Content-Type' => 'application/json; UTF-8', |
|
197 |
+ }, :status => 200) |
|
198 |
+ site = { |
|
199 |
+ 'name' => "Some JSON Response", |
|
200 |
+ 'expected_update_period_in_days' => 2, |
|
201 |
+ 'type' => "json", |
|
202 |
+ 'url' => "http://wrong-encoding.example.com", |
|
203 |
+ 'mode' => 'on_change', |
|
204 |
+ 'extract' => { |
|
205 |
+ 'value' => { 'path' => 'value' }, |
|
206 |
+ }, |
|
207 |
+ 'force_encoding' => 'EUC-JP', |
|
208 |
+ } |
|
209 |
+ checker = Agents::WebsiteAgent.new(:name => "Wrong Encoding Site", :options => site) |
|
210 |
+ checker.user = users(:bob) |
|
211 |
+ checker.save! |
|
212 |
+ |
|
213 |
+ checker.check |
|
214 |
+ event = Event.last |
|
215 |
+ event.payload['value'].should == huginn |
|
216 |
+ end |
|
84 | 217 |
end |
85 | 218 |
|
86 | 219 |
describe '#working?' do |
@@ -241,11 +374,26 @@ describe Agents::WebsiteAgent do |
||
241 | 374 |
end |
242 | 375 |
end |
243 | 376 |
end |
377 |
+ |
|
378 |
+ describe "#receive" do |
|
379 |
+ it "should scrape from the url element in incoming event payload" do |
|
380 |
+ @event = Event.new |
|
381 |
+ @event.agent = agents(:bob_rain_notifier_agent) |
|
382 |
+ @event.payload = { 'url' => "http://xkcd.com" } |
|
383 |
+ |
|
384 |
+ lambda { |
|
385 |
+ @checker.options = @site |
|
386 |
+ @checker.receive([@event]) |
|
387 |
+ }.should change { Event.count }.by(1) |
|
388 |
+ end |
|
389 |
+ end |
|
244 | 390 |
end |
245 | 391 |
|
246 | 392 |
describe "checking with http basic auth" do |
247 | 393 |
before do |
248 |
- stub_request(:any, /user:pass/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/xkcd.html")), :status => 200) |
|
394 |
+ stub_request(:any, /example/). |
|
395 |
+ with(headers: { 'Authorization' => "Basic #{['user:pass'].pack('m').chomp}" }). |
|
396 |
+ to_return(:body => File.read(Rails.root.join("spec/data_fixtures/xkcd.html")), :status => 200) |
|
249 | 397 |
@site = { |
250 | 398 |
'name' => "XKCD", |
251 | 399 |
'expected_update_period_in_days' => 2, |
@@ -271,4 +419,32 @@ describe Agents::WebsiteAgent do |
||
271 | 419 |
end |
272 | 420 |
end |
273 | 421 |
end |
422 |
+ |
|
423 |
+ describe "checking with headers" do |
|
424 |
+ before do |
|
425 |
+ stub_request(:any, /example/). |
|
426 |
+ with(headers: { 'foo' => 'bar', 'user_agent' => /Faraday/ }). |
|
427 |
+ to_return(:body => File.read(Rails.root.join("spec/data_fixtures/xkcd.html")), :status => 200) |
|
428 |
+ @site = { |
|
429 |
+ 'name' => "XKCD", |
|
430 |
+ 'expected_update_period_in_days' => 2, |
|
431 |
+ 'type' => "html", |
|
432 |
+ 'url' => "http://www.example.com", |
|
433 |
+ 'mode' => 'on_change', |
|
434 |
+ 'headers' => { 'foo' => 'bar' }, |
|
435 |
+ 'extract' => { |
|
436 |
+ 'url' => { 'css' => "#comic img", 'attr' => "src" }, |
|
437 |
+ } |
|
438 |
+ } |
|
439 |
+ @checker = Agents::WebsiteAgent.new(:name => "ua", :options => @site) |
|
440 |
+ @checker.user = users(:bob) |
|
441 |
+ @checker.save! |
|
442 |
+ end |
|
443 |
+ |
|
444 |
+ describe "#check" do |
|
445 |
+ it "should check for changes" do |
|
446 |
+ lambda { @checker.check }.should change { Event.count }.by(1) |
|
447 |
+ end |
|
448 |
+ end |
|
449 |
+ end |
|
274 | 450 |
end |
@@ -0,0 +1,31 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+shared_examples_for JsonPathOptionsOverwritable do |
|
4 |
+ before(:each) do |
|
5 |
+ @valid_params = described_class.new.default_options |
|
6 |
+ |
|
7 |
+ @checker = described_class.new(:name => "somename", :options => @valid_params) |
|
8 |
+ @checker.user = users(:jane) |
|
9 |
+ |
|
10 |
+ @event = Event.new |
|
11 |
+ @event.agent = agents(:bob_weather_agent) |
|
12 |
+ @event.payload = { :room_name => 'test room', :message => 'Looks like its going to rain', username: "Huggin user"} |
|
13 |
+ @event.save! |
|
14 |
+ end |
|
15 |
+ |
|
16 |
+ describe "select_option" do |
|
17 |
+ it "should use the room_name_path if specified" do |
|
18 |
+ @checker.options['room_name_path'] = "$.room_name" |
|
19 |
+ @checker.send(:select_option, @event, :room_name).should == "test room" |
|
20 |
+ end |
|
21 |
+ |
|
22 |
+ it "should use the normal option when the path option is blank" do |
|
23 |
+ @checker.options['room_name'] = 'test' |
|
24 |
+ @checker.send(:select_option, @event, :room_name).should == "test" |
|
25 |
+ end |
|
26 |
+ end |
|
27 |
+ |
|
28 |
+ it "should merge all options" do |
|
29 |
+ @checker.send(:merge_json_path_options, @event).symbolize_keys.keys.should == @checker.send(:options_with_path) |
|
30 |
+ end |
|
31 |
+end |
@@ -0,0 +1,53 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+shared_examples_for WorkingHelpers do |
|
4 |
+ describe "recent_error_logs?" do |
|
5 |
+ it "returns true if last_error_log_at is near last_event_at" do |
|
6 |
+ agent = Agent.new |
|
7 |
+ |
|
8 |
+ agent.last_error_log_at = 10.minutes.ago |
|
9 |
+ agent.last_event_at = 10.minutes.ago |
|
10 |
+ agent.recent_error_logs?.should be_true |
|
11 |
+ |
|
12 |
+ agent.last_error_log_at = 11.minutes.ago |
|
13 |
+ agent.last_event_at = 10.minutes.ago |
|
14 |
+ agent.recent_error_logs?.should be_true |
|
15 |
+ |
|
16 |
+ agent.last_error_log_at = 5.minutes.ago |
|
17 |
+ agent.last_event_at = 10.minutes.ago |
|
18 |
+ agent.recent_error_logs?.should be_true |
|
19 |
+ |
|
20 |
+ agent.last_error_log_at = 15.minutes.ago |
|
21 |
+ agent.last_event_at = 10.minutes.ago |
|
22 |
+ agent.recent_error_logs?.should be_false |
|
23 |
+ |
|
24 |
+ agent.last_error_log_at = 2.days.ago |
|
25 |
+ agent.last_event_at = 10.minutes.ago |
|
26 |
+ agent.recent_error_logs?.should be_false |
|
27 |
+ end |
|
28 |
+ end |
|
29 |
+ describe "received_event_without_error?" do |
|
30 |
+ before do |
|
31 |
+ @agent = Agent.new |
|
32 |
+ end |
|
33 |
+ |
|
34 |
+ it "should return false until the first event was received" do |
|
35 |
+ @agent.received_event_without_error?.should == false |
|
36 |
+ @agent.last_receive_at = Time.now |
|
37 |
+ @agent.received_event_without_error?.should == true |
|
38 |
+ end |
|
39 |
+ |
|
40 |
+ it "should return false when the last error occured after the last received event" do |
|
41 |
+ @agent.last_receive_at = Time.now - 1.minute |
|
42 |
+ @agent.last_error_log_at = Time.now |
|
43 |
+ @agent.received_event_without_error?.should == false |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ it "should return true when the last received event occured after the last error" do |
|
47 |
+ @agent.last_receive_at = Time.now |
|
48 |
+ @agent.last_error_log_at = Time.now - 1.minute |
|
49 |
+ @agent.received_event_without_error?.should == true |
|
50 |
+ end |
|
51 |
+ end |
|
52 |
+ |
|
53 |
+end |
@@ -1,8 +1,13 @@ |
||
1 | 1 |
# This file is copied to spec/ when you run 'rails generate rspec:install' |
2 | 2 |
ENV["RAILS_ENV"] ||= 'test' |
3 | 3 |
|
4 |
-require 'coveralls' |
|
5 |
-Coveralls.wear!('rails') |
|
4 |
+if ENV['COVERAGE'] |
|
5 |
+ require 'simplecov' |
|
6 |
+ SimpleCov.start 'rails' |
|
7 |
+else |
|
8 |
+ require 'coveralls' |
|
9 |
+ Coveralls.wear!('rails') |
|
10 |
+end |
|
6 | 11 |
|
7 | 12 |
require File.expand_path("../../config/environment", __FILE__) |
8 | 13 |
require 'rspec/rails' |
@@ -42,4 +47,5 @@ RSpec.configure do |config| |
||
42 | 47 |
|
43 | 48 |
config.include Devise::TestHelpers, :type => :controller |
44 | 49 |
config.include SpecHelpers |
50 |
+ config.include Delorean |
|
45 | 51 |
end |